Why Another Article on Direct Draw Pixel Plotting?
Do we really need yet another article on how to draw a pixel on the screen? Yes, I think we do. Consider the following possibility: You are writing a game for Windows. You need speed (and who doesn't?), so you are using DirectDraw surfaces to display your graphics. At this point, you have two options, Exclusive Mode or Windowed Mode. If you are running your application in Exclusive Mode, you can set the bit depth to any value you want. In that case, you don't have to worry about a general routine to plot pixels. You know exactly what bit depth the screen is in, and can write a routine specifically for that case.
But let's assume that you're writing your application in Windowed Mode. In general, when you are in this mode, you cannot change the bit depth without a system reboot. Your end-users can be in any mode, from 8 to 32 bpp, and you have to support them all. Normally, end-users will get pretty upset if you force them to be in a specific bit depth. No one wants to have to reboot their computer just to play your game. This is exactly the situation I found myself in. I began to write a general pixel drawing routine. Then I got stuck, looked in vain for help on the web, asked a lot of questions, banged my head against the screen a lot, and finally, eventually, coded a working routine. I hope this article will prevent others from having to go through the same ordeal.
In this article, I will explain one possible method for correctly plotting a pixel, regardless of what bit depth the end-user is using, thereby keeping them happy, and making sure your game has a long and prosperous career on their hard drives. I am not, however, going to discuss 8 bpp mode. 8 bpp mode (or palletized mode) is a whole different beast than the higher bit modes. It is complicated, ungainly, and, for 256 colors, simply isn't worth the trouble. Further, this mode is quickly becoming obsolete, as users are moving to 16+ bpp modes. Most commercial games no longer support 8 bpp mode, and, in this day and age, it is perfectly reasonable to request your end user switch to a higher mode if 8 bpp or lower is detected.
What I am Assuming
I will assume you have some experience using DirectDraw, know how to link into the DirectX libraries, and can initialize and set up surfaces. If you are unclear on how to do this, there is a wealth of information on this subject available on the web. Also, I am assuming you are using a C++ compiler. If you are using a C compiler, you will need to modify all DirectDraw function calls to reference their method through a pointer to the object's vtable. For example, in C++, you might have:
lpSurf->GetPixelFormat(&DDpf);
while in C, you would modify this to read:
lpSurf->lpVtbl->GetPixelFormat(lpSurf, &DDpf);
The Problems Associated with Bit Depth Independent Pixel Plotting
The problem of putting a pixel on the screen via the DirectDraw Lock() function can be split into three distinct tasks. Each of these tasks is somewhat complicated by the fact that different values will be generated, depending on the user's current bit depth (for example, the same RGB value will generate different compiled pixel values in 16 bpp mode vs. 32 bpp mode).
The three tasks to plot a pixel are:
- Compiling the pixel value from a RGB value.
- Calculating the surface address.
- Writing the pixel value to the surface.
I will discuss each of these tasks individually.
Compiling the Pixel Value
When Windows programmers deal with colors, they are typically used to handling RGB triplets. A RGB triplet is simply a series of three BYTE values representing the intensity of the red, green, and blue components of the color. Because these are BYTE values, each can range from 0-255. Unfortunately, the DirectDraw surface does not work in RGB. So, the first step in drawing a pixel to the screen is to convert your RGB triplet into a value the DirectDraw surface can understand.
When we talk about bits per pixel (bpp), what we mean is that for each pixel shown on the screen, that many bits are set aside on the drawing surface for the storage of the value representing that pixel. When a user is in 16 bpp, 2 bytes are reserved on the surface for each pixel. Similarly, if they are in 24 or 32 bpp, there are 3 or 4 bytes reserved (respectively). Because the bit depth of the surface can vary from machine to machine, we need a routine which will determine the current bit depth, and convert the RGB triplet into a value with the appropriate number of bytes.
A typical pixel compiler routine returns the compiled pixel value as a DWORD. This is reasonable, since the maximum number of bytes in a pixel is 4 (when the user is in 32 bpp). In lower bpp modes, the more significant bits are ignored, and are usually set to 0. However, this method presents a problem when trying to transfer that value to a 2, 3, or 4 bpp surface. A WORD typecast could be used for 2 byte surface, but that still leaves the 3 byte surface as problematic. For this reason, the routine I am going to use first compiles the pixel into a DWORD value, then splits up the four bytes into separate variables. In this manner, the surface can be accessed using a BYTE pointer, and each byte of the pixel written separately.
The first step in compiling a pixel value is to determine which bits on the DirectDraw surface correspond to which color intensities of the desired pixel. Fortunately, DirectDraw helps us out with this with the GetPixelFormat() function. GetPixelFormat() fills in a structure (DDPIXELFORMAT) with quite a bit of information about the DirectDraw surface, including the individual color component bitmasks. A bitmask is nothing more than a series of flags showing which bits control which color components of the pixel. This sounds a lot more complicated than it actually is, and an example should serve to quickly clear up any confusion. Assume the user's machine is set at 24 bpp, with 8 bits each for each of the three colors (which is typically the case at 24 bpp). If you implemented the following code (with your surface called lpSurf):
DDPIXELFORMAT DDpf;
ZeroMemory (&DDpf, sizeof(DDpf));
DDpf.dwSize = sizeof(DDpf);
lpSurf->GetPixelFormat(&DDpf);
you would get the following values stored in the structure
DDpf.dwRBitMask 0b 0000 0000 1111 1111 0000 0000 0000 0000 (or 0x 00ff 0000)
DDpf.dwGBitMask 0b 0000 0000 0000 0000 1111 1111 0000 0000 (or 0x 0000 ff00)
DDpf.dwBBitMask 0b 0000 0000 0000 0000 0000 0000 1111 1111 (or 0x 0000 00ff)
on a different machine, set at 16 bpp (with 5-6-5 bits for red, green, and blue, respectively), you would get
DDpf.dwRBitMask 0b 0000 0000 0000 0000 1111 1000 0000 0000 (or 0x 0000 f800)
DDpf.dwGBitMask 0b 0000 0000 0000 0000 0000 0111 1110 0000 (or 0x 0000 07e0)
DDpf.dwBBitMask 0b 0000 0000 0000 0000 0000 0000 0001 1111 (or 0x 0000 001f)
this is the key to differentiating between different user's surface depths with a single routine. Notice the first set stores all of the color values in 3 bytes, while the second set stores the same color in 2.
The following routine uses the color's bitmasks to calculate values which we will use to compile the pixel value. Assuming you pass a particular color's bitmask to the routine, it will give you both the shift value (i.e. the number of zeros on the right of the bitmask), and the precision (the number of ones in the bitmask). We will use this routine to calculate our compiled pixel later on.
int GetMaskInfo (DWORD Bitmask, int* lpShift)
{
int Precision, Shift;
Precision = Shift = 0;
//count the zeros on right hand side
while (!(Bitmask & 0x01L))
{
Bitmask >>= 1;
Shift++;
}
//count the ones on right hand side
while (Bitmask & 0x01L)
{
Bitmask >>= 1;
Precision++;
}
*lpShift = Shift;
return Precision;
}
At this point, this routine should be fairly self explanatory. If 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];
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 beginning 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 bloated code). Anyway, I could go on and on. I'll leave further optimizations as a reader exercise.
Using some basic optimizations of this code, I am drawing about 10K pixels per frame, and achieving about 10 frames per second. Most of that time, however, is spent calculating the position of the pixels relative to ellipses (I am drawing ~200 ellipses per frame), which make for some very complex calculations. The actual time spent drawing the pixels amounts to about 20% of that time. The point being, you should be able to get animation speed pixel drawing (30+ fps) with these routines, once you optimize them.
Acknowledgments
The "Without Whom.." category:
Thanks to Andy Buchanan who helped me out with the compile pixel routines (which my code is based on).
Also, thanks go out to Nathan Davies, Christopher Seddon, Sam Christiansen, and Chris Barnes for writing excellent articles on 16 bpp pixel drawing in DirectDraw.
An extra thanks goes out to Nathan Davies, who allowed me to bounce ideas off of him while this code was in development.
Contact
Please do not hesitate to contact me if you have any questions or comments about this article or the code. Also, the routines shown here are heavily modified (and simplified) from my current project. In their current form, they have never actually been field tested, so it is possible that bugs have slipped into the code. If you spot anything you see as suspicious, please let me know about it so I can correct it.
You can contact me at RHiler@RJCyberware.com
All Material within this article Copyright (C) 1998 by Ron Hiler All rights reserved.
Reprinted with permission