Topic : Pixel Plotting
Author : Ron Hiler
Page : << Previous 2  Next >>
Go to page :


you pass a bitmask of 0b 0000 0000 0000 0000 1111 1000 0000 0000, it will return a Precision of 5 and a shift of 11.
Now, let’s use that routine to compile our pixel. We will be putting the compiled pixel bytes into a class or global variable for later use.


#define NumberOfColors 10     //set to however many colors you will need
//class or global variable array
Color[NumberOfColors][4];

void CompilePixel (LPDIRECTDRAWSURFACE3 lpSurf, int ColorIndex, int r, int g, int b)
{
    DDPIXELFORMAT DDpf;
    int rsz, gsz, bsz;  //bitsize of field
    int rsh, gsh, bsh; //0’s on left (the shift value)
    DWORD CompiledPixel;

    ZeroMemory (&DDpf, sizeof(DDpf));
    DDpf.dwSize = sizeof(DDpf);
    lpSurf->GetPixelFormat(&DDpf);
 
    rsz = GetMaskInfo (DDpf.dwRBitMask, &rsh);
    gsz = GetMaskInfo (DDpf.dwGBitMask, &gsh);
    bsz = GetMaskInfo (DDpf.dwBBitMask, &bsh);

    r >>= (8-rsz); //keep only the MSB bits of component
    g >>= (8-gsz);
    b >>= (8-bsz);
    r <<= RSH; //SHIFT THEM INTO PLACE
    G <<= GSH;
    B <<= BSH;

    COMPILEDPIXEL = (DWORD)(R | G | B);
    COLOR[COLORINDEX][3] = (BYTE) COMPILEDPIXEL;
    COLOR[COLORINDEX][2] = (BYTE)(COMPILEDPIXEL >>= 8);
    Color[ColorIndex][1] = (BYTE)(CompiledPixel >>= 8);
    Color[ColorIndex][0] = (BYTE)(CompiledPixel >>= 8);
    return;
}



This routine takes a DirectDraw surface, the color index you are setting, and the red, green, and blue values you want compiled, and stores the four bytes corresponding to the compiled pixel value in a class variable (or global, if you prefer). We’ll quickly take a look at each section of this routine.


ZeroMemory (&DDpf, sizeof(DDpf));
DDpf.dwSize = sizeof(DDpf);
lpSurf->GetPixelFormat(&DDpf);



This simply takes our surface and fills in the DDPIXELFORMAT structure, same as we did above. Note that this is horrible programming practice, due to the fact that I am ignoring the return value of the DirectDraw function. I cannot stress enough how important it is to always check DirectDraw’s return values. You will save yourself hours of headaches by following that rule. In the interest of brevity, however, I have eliminated the error checking code from these routines.


rsz = GetMaskInfo (DDpf.dwRBitMask, &rsh);
gsz = GetMaskInfo (DDpf.dwGBitMask, &gsh);
bsz = GetMaskInfo (DDpf.dwBBitMask, &bsh);



Here we are using our GetMaskInfo() routine to retrieve the shift and precision value of each of the three color components for a particular user’s machine. We will be using these values to compile our pixel.


r >>= (8-rsz); //keep only the MSB bits of component
g >>= (8-gsz);
b >>= (8-bsz);



This part of the routine only comes into play in 16 bpp mode. In 24 and 32 bpp, the precision is greater than or equal to 8, so no shift occurs. But in 16 bpp, the three RGB bytes must be compressed into two bytes. To achieve this, the low order bits are dropped until the color component fits into it’s designated bit size (usually 5 or 6 bits).


r <<= RSH; //SHIFT THEM INTO PLACE
G <<= GSH;
B <<= BSH;
COMPILEDPIXEL = (DWORD)(R | G | B);



Here, the color components are shifted back to the left (using the shift value determined by our first routine), until they are properly placed, and then ORed together, to create a single value. At this point, we have our compiled pixel value.


Color[ColorIndex][3] = (BYTE) CompiledPixel;
Color[ColorIndex][2] = (BYTE)(CompiledPixel >>= 8);
Color[ColorIndex][1] = (BYTE)(CompiledPixel >>= 8);
Color[ColorIndex][0] = (BYTE)(CompiledPixel >>= 8);
return;



Now, our newly compiled pixel is broken up into BYTE size chunks to make our life easier when we get to the surface writing routines. That’s it! We are now ready to determine where to put the pixel, and write these values to the DirectDraw surface.

Calculating the Surface Address

Determining the surface address is fairly straightforward. The following routine takes a DirectDraw Surface, the Surface Pitch (explained below), an X, and a Y value, and returns the address. Note that this routine assumes point 0,0 is in the upper left corner of your surface. The absolute address of the surface will be determined during the write routine.


int CalculateSurfaceAddress (DDSURFACE3 lpSurf, LONG lPitch, int x, int y)
{
    DDPIXELFORMAT DDpf;
    int BytesPerPixel;

    //fill the DDpf structure and get the BytesPerPixel
    ZeroMemory (&DDpf, sizeof(DDpf));
    DDpf.dwSize = sizeof(DDpf);
    lpSurf->GetPixelFormat(&DDpf);
    BytesPerPixel = DDpf.dwRGBBitCount/8;

    //calculate the surface address
    return (x * BytesPerPixel) + (lPitch * y);
}



That’s all there is to it. In the final calculation, the lPitch is the distance, in bytes, between one pixel and the pixel directly below it. Those of you familiar with the DDSURFACEDESC structure might be tempted to use dwWidth instead of lPitch here, however, this is not good practice. The dwWidth value is the width of the surface in bytes. However, many video cards pad the end of each line with extra bytes to hit WORD or DWORD boundaries. These are not taken into account in the dwWidth value, while they are accounted for in the lPitch value. Using dwWidth might work on your machine, but it certainly won’t work on all machines, while lPitch will.
Remember that we are going to access our surface with a BYTE* pointer, so we need to multiply our x value by the number of bytes reserved for each pixel (so if you want to draw to the second pixel over on a 3 byte per pixel surface, you actually need to start writing at byte 6). The bytes per pixel is already taken into account in the lPitch value, which is why we are not multiplying the y value also (keep in mind, lPitch is in bytes, not in pixels).

Placing the Pixel Value on the Memory Surface

Now that we know what to place on the surface, and where to place it, it’s time to actually draw that pixel. Again, we’ve already laid the groundwork, so actually writing the pixel is not a big deal. Let’s get on with it.


void DrawPixel (DDSURFACE3 lpSurf, int x, int y, int color)
{
    DDSURFACEDESC LockedSurface;
    BYTE* LockedSurfaceMemory;
    int SurfacePoint;
    DDPIXELFORMAT DDpf;
    int BytesPerPixel;

    //fill the DDpf structure and get the BytesPerPixel
    ZeroMemory (&DDpf, sizeof(DDpf));
    DDpf.dwSize = sizeof(DDpf);
    lpSurf->GetPixelFormat(&DDpf);
    BytesPerPixel = DDpf.dwRGBBitCount/8;

    lpSurf->Lock(NULL, &LockedSurface, DDLOCK_WAIT |  DDLOCK_NOSYSLOCK, NULL);
    LockedSurfaceMemory = (BYTE*)LockedSurface.lpSurface; 
    SurfacePoint = CalculateSurfaceAddress (lpSurf, LockedSurface.lPitch x, y);
    for (int i=3; i>=(4-BytesPerPixel); i--)
    {
        LockedSurfaceMemory[SurfacePoint] = Color[color][i];
        SurfacePoint++;
    }
    lpSurf->Unlock(LockedSurface.lpSurface);
    return;
}



Here, the Lock() call locks down the memory and fills the DDSURFACEDESC structure, giving you, among other things, a pointer to the begining of the actual surface. We then BYTE typecast that pointer into LockedSurfaceMemory, in order to access the surface a single byte at a time. We calculate the relative address of the x and y pixel using our previous routine, and use that value as an array pointer onto the surface.

The for loop sends the appropriate color byte to the surface. You’ll notice it’s structured to stop after writing only the appropriate number of bytes (2, 3 or 4), thus making sure we don’t overwrite any pixel values we shouldn’t. Once the pixel has been written, we release the lock on the surface.

There you go. A single general routine to write a color to a particular pixel, independent of the user’s current bit depth.

Optimizations

There are literally dozens of places these routines could be improved for speed. I wrote these routines for clarity, not performance. For one thing, I would never put the CalculateSurfaceAddress function into it’s own routine. At the very least, you should make this an inline function. Also, having a routine which writes one pixel per Lock is extremely wasteful. The Lock call is very expensive, in terms of time. Although you should attempt to keep the time between Lock and Unlock calls to a minimum (for various reasons), it is not unreasonable to draw an entire frame before unlocking the surface memory. Also, I've called GetPixelFormat() in practically every routine. Complete waste of time. Call it once, and store the results somewhere. Unless you loose your surface, they aren't going to change.

By their very nature, general routines are slower than specific routines. If you know you are writing for 32 bpp, you can eliminate whole chunks of this code. By the same token, using case statements for 16, 24, and 32 bit surfaces might get you faster results (though quite a bit more

Page : << Previous 2  Next >>