JavaScript EditorFree JavaScript Editor     Ajax Editor 



Main Page
  Previous Section Next Section

Building a Display Surface

As 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.

graphics/06fig07.gif

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 Surface

All right, to create any surface, you must follow these steps:

  1. Fill out a DDSURFACEDESC2 data structure that describes the surface you want to create.

  2. Call IDirectDraw7::CreateSurface() to create the surface.

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);

dwFlagsThis 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.

Table 6.5. The Various Flags for the dwFlags Field of DDSURFACEDESC2
Value Description
DDSD_ALPHABITDEPTH Indicates that the dwAlphaBitDepth member is valid.
DDSD_BACKBUFFERCOUNT Indicates that the dwBackBufferCount member is valid.
DDSD_CAPS Indicates that the ddsCaps member is valid.
DDSD_CKDESTBLT Indicates that the ddckCKDestBlt member is valid.
DDSD_CKDESTOVERLAY Indicates that the ddckCKDestOverlay member is valid.
DDSD_CKSRCBLT Indicates that the ddckCKSrcBlt member is valid.
DDSD_CKSRCOVERLAY Indicates that the ddckCKSrcOverlay member is valid.
DDSD_HEIGHT Indicates that the dwHeight member is valid.
DDSD_LINEARSIZE Indicates that the dwLinearSize member is valid.
DDSD_LPSURFACE Indicates that the lpSurface member is valid.
DDSD_MIPMAPCOUNT Indicates that the dwMipMapCount member is valid.
DDSD_PITCH Indicates that the lPitch member is valid.
DDSD_PIXELFORMAT Indicates that the ddpfPixelFormat member is valid.
DDSD_REFRESHRATE Indicates that the dwRefreshRate member is valid.
DDSD_TEXTURESTAGE Indicates that the dwTextureStage member is valid.
DDSD_WIDTH Indicates that the dwWidth member is valid.

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.

graphics/06fig08.gif

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…

Table 6.6. Capabilities Control Settings for DirectDraw Surfaces
Value Description
DDSCAPS_BACKBUFFER Indicates that this surface is the back buffer of a surface flipping structure.
DDSCAPS_COMPLEX Indicates that a complex surface is being described. A complex surface is a surface with a primary surface and one or more back buffers to create a flipping chain.
DDSCAPS_FLIP Indicates that this surface is a part of a surface flipping structure. When this capability is passed to the CreateSurface() method, a front buffer and one or more back buffers are created.
DDSCAPS_LOCALVIDMEM Indicates that this surface exists in true, local video memory rather than non-local video memory. If this flag is specified, DDSCAPS_VIDEOMEMORY must be specified as well.
DDSCAPS_MODEX Indicates that this surface is a 320x200 or 320x240 Mode X surface.
DDSCAPS_NONLOCALVIDMEM Indicates that this surface exists in non-local video memory rather than true, local video memory. If this flag is specified, DDSCAPS_VIDEOMEMORY flag must be specified as well.
DDSCAPS_OFFSCREENPLAIN Indicates that this surface is an offscreen surface that is not a special surface such as an overlay, texture, z-buffer, front-buffer, back-buffer, or alpha surface. Usually used for sprites.
DDSCAPS_OWNDC Indicates that this surface will have a device context association for a long period.
DDSCAPS_PRIMARYSURFACE Indicates that this surface is the primary surface. It represents what is visible to the user at the moment.
DDSCAPS_STANDARDVGAMODE Indicates that this surface is a standard VGA mode surface, and not a Mode X surface. This flag cannot be used in combination with the DDSCAPS_MODEX flag.
DDSCAPS_SYSTEMMEMORY Indicates that this surface memory was allocated in system memory.
DDSCAPS_VIDEOMEMORY Indicates that this surface exists in display memory.

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 Palette

In 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 Pixels

To 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.

graphics/06fig09.gif

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.

graphics/06fig10.gif

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(…)

graphics/06fig11.gif

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.

Table 6.7. The Control Flags for the Lock() Method
Value Description
DDLOCK_READONLY Indicates that the surface being locked will be read-only.
DDLOCK_SURFACEMEMORYPTR Indicates that a valid memory pointer to the top of the specified RECT should be returned. If no rectangle is specified, a pointer to the top of the surface is returned. This is the default.
DDLOCK_WAIT If a lock cannot be obtained because a blit operation is in progress, the method retries until a lock is obtained or another error occurs, such as DDERR_SURFACEBUSY.
DDLOCK_WRITEONLY Indicates that the surface being locked will be write-enabled.

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.

graphics/06fig12.gif

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 Up

Before 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.


      Previous Section Next Section
    
    lawrence factor . Cultural Travel


    JavaScript EditorAjax Editor     JavaScript Editor