Building a Display SurfaceAs you know, the image displayed on the screen is nothing more than a matrix of colored pixels represented in memory for some format, either palletized or RGB. In either case, to make anything happen, you need to know how to draw into this memory. However, under DirectDraw the designers decided to abstract the concept of video memory just a little bit so that no matter how weird the video card in your system (or someone else's) is, accessing the video surfaces will be the same for you (the programmer's point of view). Thus, DirectDraw supports what are called surfaces. Referring to Figure 6.7, surfaces are rectangular regions of memory that can hold bitmap data. Furthermore, there are two kinds of surfaces: primary and secondary. Figure 6.7. Surfaces can be any size.A primary surface directly corresponds to the actual video memory being rasterized by the video card and is visible at all times. Hence, you will have only one primary surface in any DirectDraw program, and it refers directly to the screen image and usually resides in VRAM. When you manipulate it, you see the results instantly on the screen. For example, if you set the video mode to 640x480x256, you must create a primary surface that is also 640x480x256 and then attach it to the display device—the IDirectDraw7 object. Secondary surfaces, on the other hand, are much more flexible. They can be any size, can reside in either VRAM or system memory, and you can create as many of them as memory will allow. In most cases, you will create one or two secondary surfaces (back buffers) for smooth animation. These will always have the same color depth and geometry as the primary surface. Then you update these offscreen surfaces with the next frame of animation, and then quickly copy or page flip the offscreen surface into the primary surface for smooth animation. This is called double or triple buffering. You'll learn more on this in the next chapter, but that's one use for secondary surfaces. The second use for secondary surfaces is to hold your bitmap images and animations that represent objects in the game. This is a very important feature of DirectDraw because only by using DirectDraw surfaces can you invoke hardware acceleration on bitmap data. If you write your own bit blitting (bitmap image transferring) software to write bitmaps, you lose all acceleration. Now, I'm getting a little ahead of myself here, so I want to come out of warp and back down to sub-light speed. I just wanted to get you thinking a bit. For now, let's just see how to create a simple primary surface that's the same size as your display mode, and then you'll learn to write data to it and plot pixels on the screen. Creating a Primary SurfaceAll right, to create any surface, you must follow these steps:
Here's the prototype for CreateSurface(): HRESULT CreateSurface( LPDDSURFACEDESC2 lpDDSurfaceDesc2, LPDIRECTDRAWSURFACE4 FAR *lplpDDSurface, IUnknown FAR *pUnkOuter); Basically, the function takes a DirectDraw surface description of the surface you want to create, a pointer to receive the interface, and finally NULL for the advanced COM feature pUnkOuter. Huh? Filling out the data structure can be a bit bewildering, but I'll step you through it. First, let's take a look at the DDSURFACEDESC2: typedef struct _DDSURFACEDESC2 { DWORD dwSize; // size of this structure DWORD dwFlags; // control flags DWORD dwHeight; // height of surface in pixels DWORD dwWidth; // width of surface in pixels union { LONG lPitch; // memory pitch per row DWORD dwLinearSize; // size of the buffer in bytes } DUMMYUNIONNAMEN(1); DWORD dwBackBufferCount; // number of back buffers chained union { DWORD dwMipMapCount; // number of mip-map levels DWORD dwRefreshRate; // refresh rate } DUMMYUNIONNAMEN(2); DWORD dwAlphaBitDepth; // number of alpha bits DWORD dwReserved; // reserved LPVOID lpSurface; // pointer to surface memory DDCOLORKEY ddckCKDestOverlay; // dest overlay color key DDCOLORKEY ddckCKDestBlt; // destination color key DDCOLORKEY ddckCKSrcOverlay; // source overlay color key DDCOLORKEY ddckCKSrcBlt; // source color key DDPIXELFORMAT ddpfPixelFormat; // pixel format of surface DDSCAPS2 ddsCaps; // surface capabilities DWORD dwTextureStage; // used to bind a texture // to specific stage of D3D } DDSURFACEDESC2, FAR* LPDDSURFACEDESC2; As you can see, this is a complicated structure. Moreover, 75 percent of the fields are more than cryptic. Luckily, you only need to know about the ones that I've bolded. Let's take a look at their functions in detail, one by one: dwSize— This is one of the most important fields in any DirectX data structure. Many DirectX data structures are sent by address, so the receiving function or method doesn't know the size of the data structure. However, if the first 32-bit value is always the size of the data structure, the receiving function will always know how much data is there just by dereferencing the first DWORD. Hence, DirectDraw and DirectX data structures in general have the size specifier as the first element of all structures. It may seem redundant, but it's a good design—trust me. All you need to do is fill it in like this: DDSURFACEDESC2 ddsd; ddsd.dwSize = sizeof(DDSURFACEDESC2); dwFlags— This field is used to indicate to DirectDraw which fields you'll be filling in with valid info or, if you're using this structure in a query operation, which fields you want to retrieve. Take a look at Table 6.5 for the possible values that the flags word can take on. For example, if you were going to place valid data in the dwWidth and dwHeight fields, you would set the dwFlags field like this: ddsd.dwFlags = DDSD_WIDTH | DDSD_HEIGHT; Then DirectDraw would know to look in the dwHeight and dwWidth fields and that the data would be valid. Think of dwFlags as a valid data specifier. dwWidth— Indicates the width of the surface in pixels. When you create a surface, this is where you set the width—320, 640, and so on. In addition, if you query the properties of a surface, this field will return the width of the surface (if you requested it). dwHeight— Indicates the height of the surface in pixels. Similarly to dwWidth, this is where you set the height of the surface you are creating—200, 240, 480, and so on. lPitch— This is an interesting field. It's basically the horizontal memory pitch of the display mode that you're in. Referring to Figure 6.8, the lPitch is the number of bytes per line for the video mode, also referred to as the stride or memory width. However you pronounce it, the bottom line is that this is a very important piece of data for the following reason: When you request a video mode like 640x480x8, you know that there are 640 pixels per line and each pixel is 8 bits (or 1 byte). Therefore, there should be exactly 640 bytes per line, and hence lPitch should be 640. Right? Not necessarily. Figure 6.8. Accessing a surface.TIP lPitch could be anything due to the layout of VRAM and thus when you advance line to line to access the memory in a DirectDraw surface you must use the lPitch to move to the next line rather than the width times the number of bytes per pixel, this is VERY important! Most new video boards support what are called linear memory modes and have addressing hardware, so this property holds true, but it's not guaranteed. Therefore, you can't assume that a 640x480x8 video mode has 640 bytes per line. This is what the lPitch field is for. You must refer to it to make your memory addressing calculations correct, so that you can move from line to line. For example, to access any pixel in a 640x480x8 (256-color) display mode, you can use the following code, assuming you've already requested DirectDraw to give you lPitch and lpSurface is pointing to the surface memory (which I'll explain next): ddsd.lpSurface[x + y*ddsd.lPitch] = color; Simple, isn't it? In most cases, ddsd.lPitch would be 640 for a 640x480x8 mode, and for a 640x480x16 mode, ddsd.lPitch would be 1280 (two bytes per pixel = 640x2). But for some cards, this may not be the case due to the way memory is stored on the card, the internal cache for the card, or whatever… The moral of the story is: Always use lPitch for your memory calculations and you'll always be safe. TRICK Even though lPitch may not equal the horizontal resolution of the mode that you set, it may be worth it to test for it so that you can switch to more optimized functions. For example, during the initialization of your code, you might get lPitch and compare it to the selected horizontal resolution. If they are equal, you might switch to highly optimized code that hard-codes the number of bytes per line. lpSurface— This field is used to retrieve a pointer to the actual memory that the surface you create resides in. The memory may be in VRAM or system memory, but you don't need to worry about it. Once you have the pointer to it, you can manipulate it as you would any other memory—write to it, read from it, and so on. This is exactly how you're going to implement pixel plotting. Alas, making this pointer valid takes a little work, but we'll get there in a minute. Basically, you must "lock" the surface memory and tell DirectX that you're going to muck with it and that no other process should attempt to read or write from it. Furthermore, when you do get this pointer, depending on the color depth—8, 16, 24, 32 bpp—you will usually cast and assign it to a working alias pointer. dwBackBufferCount— This field is used to set or read the number of back buffers or secondary offscreen flipping buffers that are chained to the primary surface. If you'll recall, back buffers are used to implement smooth animation by creating one or more virtual primary buffers (buffers with the same geometry and color depth) that are offscreen. Then you draw on the back buffer, which is invisible to the user, and then quickly flip or copy the back buffer(s) to the primary buffer for display. If you have only one back buffer, the technique is called double buffering. Using two back buffers is called triple buffering, which is a little better but memory-intensive. To keep things simple, in most cases you'll create flipping chains that contain a single primary surface and one back buffer. ddckCKDestBlt— This field is used to control the destination color key, which is used in blitting operations to control the color(s) that can be written to. More on this later in the Chapter 7, "Advanced DirectDraw and Bitmapped Graphics." ddckCKSrcBlt— This field is used to indicate the source color key, which is basically the colors that you don't want to be blitted when you're performing bitmapping operations. This is how you set the transparent colors for your bitmaps. More on this in Chapter 7. ddpfPixelFormat— This field is used to retrieve the pixel format of a surface, which is quite important if you're trying to figure out what the properties of a surface are. The following is the general structure, but you'll have to look at the DirectX SDK for all the details because they're lengthy and not really relevant right now: typedef struct _DDPIXELFORMAT { DWORD dwSize; DWORD dwFlags; DWORD dwFourCC; union { DWORD dwRGBBitCount; DWORD dwYUVBitCount; DWORD dwZBufferBitDepth; DWORD dwAlphaBitDepth; DWORD dwLuminanceBitCount; // new for DirectX 6.0 DWORD dwBumpBitCount; // new for DirectX 6.0 } DUMMYUNIONNAMEN(1); union { DWORD dwRBitMask; DWORD dwYBitMask; DWORD dwStencilBitDepth; // new for DirectX 6.0 DWORD dwLuminanceBitMask; // new for DirectX 6.0 DWORD dwBumpDuBitMask; // new for DirectX 6.0 } DUMMYUNIONNAMEN(2); union { DWORD dwGBitMask; DWORD dwUBitMask; DWORD dwZBitMask; // new for DirectX 6.0 DWORD dwBumpDvBitMask; // new for DirectX 6.0 } DUMMYUNIONNAMEN(3); union { DWORD dwBBitMask; DWORD dwVBitMask; DWORD dwStencilBitMask; // new for DirectX 6.0 DWORD dwBumpLuminanceBitMask; // new for DirectX 6.0 } DUMMYUNIONNAMEN(4); union { DWORD dwRGBAlphaBitMask; DWORD dwYUVAlphaBitMask; DWORD dwLuminanceAlphaBitMask; // new for DirectX 6.0 DWORD dwRGBZBitMask; DWORD dwYUVZBitMask; } DUMMYUNIONNAMEN(5); } DDPIXELFORMAT, FAR* LPDDPIXELFORMAT; NOTE I have bolded some of the more commonly used fields. ddsCaps— This field is used to indicate the requested properties of the surface that haven't been defined elsewhere. In reality, this field is another data structure. DDSCAPS2 is shown here: typedef struct _DDSCAPS2 { DWORD dwCaps; // Surface capabilities DWORD dwCaps2; // More surface capabilities DWORD dwCaps3; // future expansion DWORD dwCaps4; // future expansion } DDSCAPS2, FAR* LPDDSCAPS2; In 99.9 percent of all cases, you will set only the first field, dwCaps. dwCaps2 is for 3D stuff, and the remaining fields, dwCaps3 and dwCaps4, are future expansion and unused. In any case, a partial list of the possible flag settings for the dwCaps are shown in Table 6.6. For a complete listing, take a look at the DirectX SDK. For example, when creating a primary surface you would set ddsd.ddsCaps like this: ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; I know this may seem overly complex, and in some ways it is. Having doubly nested control flags is a bit of a pain, but oh well… Now that you have an idea of the complexity and power that DirectDraw gives you when you're creating surfaces, let's put the knowledge to work and create a simple primary surface that's the same size and color depth as the display mode (default behavior). Here's the code to create a primary surface: // interface pointer to hold primary surface, note that // it's the 7th revision of the interface LPDIRECTDRAWSURFACE7 lpddsprimary = NULL; DDSURFACEDESC2 ddsd; // the DirectDraw surface description // MS recommends clearing out the structure memset(&ddsd,0,sizeof(ddsd)); // could use ZeroMemory() // now fill in size of structure ddsd.dwSize = sizeof(ddsd); // enable data fields with values from table 6.5 that we // will send valid data in // in this case only the ddsCaps field is enabled, we // could have enabled the width, height etc., but they // aren't needed since primary surfaces take on the // dimensions of the display mode by default ddsd.dwFlags = DDSD_CAPS; // now set the capabilities that we want from table 6.6 ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; // now create the primary surface if (FAILED(lpdd->CreateSurface(&ddsd, &lpddsprimary, NULL))) { // error } // end if If the function was successful, lpddsprimary will point to the new surface interface and you can call methods on it (of which there are quite a few, such as attaching the palette in 256-color modes). Let's take a look at this to bring the palette example back full-circle. Attaching the PaletteIn the previous section on palettes, you did everything except attach the palette to a surface. You created the palette and filled it with entries, but you couldn't attach the palette to a surface because you didn't have one yet. Now that you have a surface (the primary), you can complete this step. To attach a palette to any surface, all you need to do is use the IDirectDrawSurface7::SetPalette() function, which is shown here: HRESULT SetPalette(LPDIRECTDRAWPALETTE lpDDPalette); This function simply takes a pointer to the palette that you want to be attached. Using the same palette that you created in the previous palette section, here's how you would associate the palette with the primary surface: if (FAILED(lpddsprimary->SetPalette(lpddpal))) { // error } // end if Not too bad, huh? At this point, you have everything you need to emulate the entire power of a DOS32 game. You can switch video modes, set the palette, and create a primary drawing surface that represents the active video image. However, there are still some details that you have to learn about, like actually locking the primary surface memory and gaining access to the VRAM and plotting a pixel. Let's take a look at that now. Plotting PixelsTo plot a pixel (or pixels) in a full-screen DirectDraw mode, you first must set up DirectDraw, set the cooperation level, set a display mode, and create at least a primary surface. Then you have to gain access to the primary surface and write to the video memory. However, before you learn how to do this, let's take another look at how video surfaces work. If you'll recall, all DirectDraw video modes and surfaces are linear, as shown in Figure 6.9. This means that memory increases from left to right and from top to bottom as you move from row to row. Figure 6.9. DirectDraw surfaces are linear.TIP You may be wondering how DirectDraw can magically turn a nonlinear video mode into a linear one if the video card itself doesn't support it. For example, Mode X is totally nonlinear and bank-switched. Well, the truth is this—when DirectDraw detects that a mode is nonlinear in hardware, a driver called VFLATD.VXD is invoked, which creates a software layer between you and the VRAM and makes the VRAM look linear. Keep in mind that this is going to be slow. In addition, to locate any position in the video buffer, you need only two pieces of information: the memory pitch per line (that is, how many bytes make up each row) and the size of each pixel (8-bit, 16-bit, 24-bit, 32-bit). You can use the following formula: // assume this points to VRAM or the surface memory UCHAR *video_buffer8; video_buffer8[x + y*memory_pitchB] = pixel_color_8; Of course, this is not exactly true because this formula works only for 8-bit modes, or modes that have one BYTE per pixel. For a 16-bit mode, or two BYTEs per pixel, you would have to do something like this: // assume this points to VRAM or the surface memory USHORT *video_buffer16; video_buffer16[x + y*(memory_pitchB >> 1)] = pixel_color_16; There's a lot going on here, so let's take a look at the code carefully. Since we're in a 16-bit mode, I'm using a USHORT pointer to the VRAM. What this does is let me use array access, but with 16-bit pointer arithmetic. Hence, when I say video_buffer16[1] this really accesses the second SHORT or byte pair 2,3. In addition, because memory_pitchB is in bytes, you must divide it by two by shifting right one bit so that it's in SHORT or 16-bit memory pitch. Finally, the assignment of pixel_color16 is also misleading because now a complete 16-bit USHORT will be written into the video buffer, rather than a single 8-bit value as in the previous example. Moreover, the 8-bit value would be a color index, whereas a 16-bit value must be a RGB value, usually encoded in R5G6B5 format or five bits for red, six bits for green, and five bits for blue, as shown in Figure 6.10. Figure 6.10. Possible 16-bit RGB encodings, including 5.6.5 format.Here's a macro to make up a 16-bit RGB word in 5.5.5 and 5.6.5 format: // this builds a 16 bit color value in 5.5.5 format (1-bit alpha mode) #define _RGB16BIT555(r,g,b) ((b & 31) + ((g & 31) << 5) + ((r & 31) << 10)) // this builds a 16 bit color value in 5.6.5 format (green dominate mode) #define _RGB16BIT565(r,g,b) ((b & 31) + ((g & 63) << 5) + ((r & 31) << 11)) As you can see, 16-bit modes and RGB modes in general have a little more complex addressing and manipulation than do the 256-color 8-bit modes, so let's begin there. To gain access to any surface—primary, secondary, and so on—you must lock and unlock the memory. This lock and unlock sequence is necessary for two reasons: First, to tell DirectDraw that you are in control of the memory (that is, it shouldn't be accessed by other processes), and second, to indicate to the video hardware that it shouldn't move any cache or virtual memory buffers around while you're messing with the locked memory. Remember, there is no guarantee that VRAM will stay in the same place. It could be virtual, but when you lock it, the memory will stay in the same address space for the duration of the lock so you can manipulate it. The function to lock memory is called IDirectDrawSurface7::Lock() and is shown here: HRESULT Lock(LPRECT lpDestRect, // destination RECT to lock LPDDSURFACEDESC2 lpDDSurfaceDesc, // address of struct to receive info DWORD dwFlags, // request flags HANDLE hEvent); // advanced, make NULL The parameters aren't that bad, but there are some new players. Let's step through them. The first parameter is the RECT of the region of surface memory that you want to lock; take a look at Figure 6.11. DirectDraw allows you to lock only a certain portion of surface memory so that, if another process is accessing a region that you aren't, processing can continue. This is great if you know that you're going to update only a certain part of the surface and don't need a full lock on the entire surface. However, in most cases you'll just lock the entire surface to keeps things simple. This is accomplished by passing NULL. Figure 6.11. IDirectDrawSurface 7->Lock(…)The second parameter is the address of a DDSURFACEDESC2 that will be filled with information about the surface that you request. Basically, just send a blank DDSURFACEDESC2 and that's it. The next parameter, dwFlags, tells Lock() what you want to do. Table 6.7 contains a list of the most commonly used values. NOTE I have bolded the most commonly used flags. The last parameter is to facilitate an advanced feature that Win32 supports called events. Set it to NULL. Locking the primary surface is really easy. What you want to do is request the memory pointer to the surface, along with requesting DirectDraw to wait for the surface to become available. Here's the code: DDSURFACEDESC2 ddsd; // this will hold the results of the lock // clear the surface description out always memset(&ddsd, 0, sizeof(ddsd)); // set the size field always ddsd.dwSize = sizeof(ddsd); // lock the surface if (FAILED(lpddsprimary->Lock(NULL, &ddsd, DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT,NULL))) { // error } // end if // ****** at this point there are two fields that we are // concerned with: ddsd.lPitch which contains the memory // pitch in bytes per line and ddsd.lpSurface which is a // pointer to the top left corner of the locked surface Once you've locked the surface, you're free to manipulate the surface memory as you wish. The memory pitch per line is stored in ddsd.lPitch, and the pointer to the actual surface is ddsd.lpSurface. Therefore, if you're in any 8-bit mode (1 byte per pixel), the following function can be used to plot a pixel anywhere on the primary surface: inline void Plot8(int x, int y, // position of pixel UCHAR color, // color index of pixel UCHAR *buffer, // pointer to surface memory int mempitch) // memory pitch per line { // this function plots a single pixel buffer[x+y*mempitch] = color; } // end Plot8 Here's how you would call it to plot a pixel at (100,20) with color index 26: Plot8(100,20,26, (UCHAR *)ddsd.lpSurface,(int)ddsd.lPitch); Similarly, here's a 16-bit 5.6.5 RGB mode plot function: inline void Plot16(int x, int y, // position of pixel UCHAR red, UCHAR green, UCHAR, blue // RGB color of pixel USHORT *buffer, // pointer to surface memory int mempitch) // memory pitch bytes per line { // this function plots a single pixel buffer[x+y*(mempitch>>1)] = __RGB16BIT565(red,green,blue); } // end Plot16 And here's how you would plot a pixel at (300,100) with RGB value (10,14,30): Plot16(300,100,10,14,30,(USHORT *)ddsd.lpSurface,(int)ddsd.lPitch); Now, once you're done with all your video surface access for the current frame of animation, you need to unlock the surface. This is accomplished with the IDirectDrawSurface7::Unlock() method shown here: HRESULT Unlock(LPRECT lpRect); You send Unlock() the original RECT that you used in the lock command, or NULL if you locked the entire surface. In this case, here's all you would do to unlock the surface: if (FAILED(lpddsprimary->Unlock(NULL))) { // error } // end if That's all there is to it. Now, let's see all the steps put together to plot random pixels on the screen (without error detection): LPDIRECTDRAW7 lpdd = NULL; // DirectDraw 7.0 interface 7 LPDIRECTDRAWSURFACE7 lpddsprimary = NULL; // surface ptr DDSURFACEDESC2 ddsd; // surface description LPDIRECTDRAWPALETTE lpddpal = NULL; // palette interface PALETTEENTRY palette[256]; // palette storage // first create base IDirectDraw 7.0 interface DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL); // set the cooperative level for full-screen mode lpdd->SetCooperativeLevel(hwnd, DDSCL_FULLSCREEN | DDSCL_ALLOWMODEX | DDSCL_EXCLUSIVE | DDSCL_ALLOWREBOOT); // set the display mode to 640x480x256 lpdd->SetDisplayMode(640,480,8,0,0); // clear ddsd and set size memset(&ddsd,0,sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); // enable valid fields ddsd.dwFlags = DDSD_CAPS; // request primary surface ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; // create the primary surface lpdd->CreateSurface(&ddsd, &lpddsprimary, NULL); // build up the palette data array for (int color=1; color < 255; color++) { // fill with random RGB values palette[color].peRed = rand()%256; palette[color].peGreen = rand()%256; palette[color].peBlue = rand()%256; // set flags field to PC_NOCOLLAPSE palette[color].peFlags = PC_NOCOLLAPSE; } // end for color // now fill in entry 0 and 255 with black and white palette[0].peRed = 0; palette[0].peGreen = 0; palette[0].peBlue = 0; palette[0].peFlags = PC_NOCOLLAPSE; palette[255].peRed = 255; palette[255].peGreen = 255; palette[255].peBlue = 255; palette[255].peFlags = PC_NOCOLLAPSE; // create the palette object lpdd->CreatePalette(DDPCAPS_8BIT |DDPCAPS_ALLOW256 | DDPCAPS_INITIALIZE, palette,&lpddpal, NULL); // finally attach the palette to the primary surface lpddsprimary->SetPalette(lpddpal); // and you're ready to rock n roll! // lock the surface first and retrieve memory pointer // and memory pitch // clear ddsd and set size, never assume it's clean memset(&ddsd,0,sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); lpddsprimary->Lock(NULL, &ddsd, DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL); // now ddsd.lPitch is valid and so is ddsd.lpSurface // make a couple aliases to make code cleaner, so we don't // have to cast int mempitch = ddsd.lPitch; UCHAR *video_buffer = ddsd.lpSurface; // plot 1000 random pixels with random colors on the // primary surface, they will be instantly visible for (int index=0; index<1000; index++) { // select random position and color for 640x480x8 UCHAR color = rand()%256; int x = rand()%640; int y = rand()%480; // plot the pixel video_buffer[x+y*mempitch] = color; } // end for index // now unlock the primary surface lpddsprimary->Unlock(NULL); Of course, I'm leaving out all the Windows initialization and event loop stuff, but that never changes. However, to be complete, take a look at DEMO6_3.CPP and the associated executable DEMO6_3.EXE on the CD. They contain the preceding code injected into your Game Console's Game_Main() function, shown in the following listing along with the updated Game_Init(). Figure 6.12 is a screen shot of the program in action. Figure 6.12. DEMO6_3.EXE in action.int Game_Main(void *parms = NULL, int num_parms = 0) { // this is the main loop of the game, do all your processing // here // for now test if user is hitting ESC and send WM_CLOSE if (KEYDOWN(VK_ESCAPE)) SendMessage(main_window_handle,WM_CLOSE,0,0); // plot 1000 random pixels to the primary surface and return // clear ddsd and set size, never assume it's clean memset(&ddsd,0,sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); if (FAILED(lpddsprimary->Lock(NULL, &ddsd, DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL))) { // error return(0); } // end if // now ddsd.lPitch is valid and so is ddsd.lpSurface // make a couple aliases to make code cleaner, so we don't // have to cast int mempitch = (int)ddsd.lPitch; UCHAR *video_buffer = (UCHAR *)ddsd.lpSurface; // plot 1000 random pixels with random colors on the // primary surface, they will be instantly visible for (int index=0; index < 1000; index++) { // select random position and color for 640x480x8 UCHAR color = rand()%256; int x = rand()%640; int y = rand()%480; // plot the pixel video_buffer[x+y*mempitch] = color; } // end for index // now unlock the primary surface if (FAILED(lpddsprimary->Unlock(NULL))) return(0); // sleep a bit Sleep(30); // return success or failure or your own return code here return(1); } // end Game_Main //////////////////////////////////////////////////////////// int Game_Init(void *parms = NULL, int num_parms = 0) { // this is called once after the initial window is created and // before the main event loop is entered, do all your initialization // here // first create base IDirectDraw interface if (FAILED(DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL))) { // error return(0); } // end if // set cooperation to full screen if (FAILED(lpdd->SetCooperativeLevel(main_window_handle, DDSCL_FULLSCREEN | DDSCL_ALLOWMODEX | DDSCL_EXCLUSIVE | DDSCL_ALLOWREBOOT))) { // error return(0); } // end if // set display mode to 640x480x8 if (FAILED(lpdd->SetDisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,0,0))) { // error return(0); } // end if // clear ddsd and set size memset(&ddsd,0,sizeof(ddsd)); ddsd.dwSize = sizeof(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))) { // error return(0); } // end if // build up the palette data array for (int color=1; color < 255; color++) { // fill with random RGB values palette[color].peRed = rand()%256; palette[color].peGreen = rand()%256; palette[color].peBlue = rand()%256; // set flags field to PC_NOCOLLAPSE palette[color].peFlags = PC_NOCOLLAPSE; } // end for color // now fill in entry 0 and 255 with black and white palette[0].peRed = 0; palette[0].peGreen = 0; palette[0].peBlue = 0; palette[0].peFlags = PC_NOCOLLAPSE; palette[255].peRed = 255; palette[255].peGreen = 255; palette[255].peBlue = 255; palette[255].peFlags = PC_NOCOLLAPSE; // create the palette object if (FAILED(lpdd->CreatePalette(DDPCAPS_8BIT | DDPCAPS_ALLOW256 | DDPCAPS_INITIALIZE, palette,&lpddpal, NULL))) { // error return(0); } // end if // finally attach the palette to the primary surface if (FAILED(lpddsprimary->SetPalette(lpddpal))) { // error return(0); } // end if // return success or failure or your own return code here return(1); } // end Game_Init The only other detail I want to bring to your attention about the demo program code is the creation of the main window, shown here:
// create the window
if (!(hwnd = CreateWindowEx(NULL, // extended style
WINDOW_CLASS_NAME, // class
"T3D DirectX Pixel Demo", // title
WS_POPUP | WS_VISIBLE,
0,0, // initial x,y
640,480, // initial width, height
NULL, // handle to parent
NULL, // handle to menu
hinstance, // instance of this application
NULL))) // extra creation parms
return(0);
Notice that instead of using the WS_OVERLAPPEDWINDOW window style, the demo uses WS_POPUP. If you'll recall, this style is devoid of all controls and Windows GUI stuff, which is what you want for a full-screen DirectX application. Cleaning UpBefore moving on to the end of the chapter, I want to bring up a topic that I've been putting off for a while—resource management. Yuck! Anyway, this seemingly un-fun concept simply means making sure that you Release() DirectDraw or DirectX objects in general when you're done with them. For example, if you take a look at the source code in DEMO6_3.CPP, in the Game_Shutdown() function you'll see a number of Release() calls to release all the DirectDraw objects back to the operating system, and DirectDraw itself, shown here: int Game_Shutdown(void *parms = NULL, int num_parms = 0) { // this is called after the game is exited and the main event // loop while is exited, do all you cleanup and shutdown here // first the palette if (lpddpal) { lpddpal->Release(); lpddpal = NULL; } // end if // now the primary surface if (lpddsprimary) { lpddsprimary->Release(); lpddsprimary = NULL; } // end if // now blow away the IDirectDraw7 interface if (lpdd) { lpdd->Release(); lpdd = NULL; } // end if // return success or failure or your own return code here return(1); } // end Game_Shutdown In general, you should Release() objects only when you're done with them, and you should do so in reverse order of creation. For example, you created the DirectDraw object, the primary surface, and the palette, in that order, so a good rule of thumb would be to release the palette, surface, and then DirectDraw, like this: // first kill the palette if (lpddpal) { lpddpal->Release(); lpddpal = NULL; } // end if // now the primary surface if (lpddsprimary) lpddsprimary->Release(); // and finally the directdraw object itself if (lpdd) { lpdd->Release(); lpdd = NULL; } // end if WARNING Before you make a call to Release(), notice the testing to see if the interface is non-NULL. This is absolutely necessary because the interface pointer may be NULL, and releasing on a NULL pointer may cause problems if the implementers of the interface haven't thought of it. |