![]() |
Gearing Up For Games - Part 2Written by Michael T. Duffy |
IntroductionWelcome back to Gearing Up For Games. Last time we looked into how to use DIVE to blit images to the screen, and then developed a canvas class to make accessing the DIVE buffer easier. This time we will look at how palettes are handled under OS/2, and we will look at decoding and displaying the common bitmap format of PCX to our canvas object. This article builds on the previous one, so the canvas class and DIVE code from the last article will be used in this (and future) articles. Eventually all this code will go together to create a simple game, and the concepts will allow you to branch out and write more meaty games that run natively under OS/2. So without further ado... PalettesBefore I start my discussion of palettes, I must call attention to an article written for EDM/2 way back in issue 0101. Raja Thiagarajan wrote an unofficial guide to the palette manager, and I think it may have even been Raja who introduced me to EDM/2 in the first place (Thanks!). I would suggest that you read his article in addition to this one, because it covers some aspects of the palette manager which I will not touch on. I would also like to thank the many people on the Internet who helped me out when I was first learning about palettes. Palettes under OS/2 drove me crazy for several months before I could finally control them to the point that they did what I wanted them to do. Even now they occasionally surprise me; in preparing for this article I discovered that some of the palette code I had been using for a long time would only display a black window in 256 color mode, whereas it worked fine in the 32k color mode I normally use! Just when I think I completely understand palettes, they surprise me with something like this. With the information in this article, the article in EDM/2 issue 1-1, and the API specifications in the manuals you should have enough information to experiment around with palettes. The approach to palettes that I present here seems to work best for games that use DIVE, though there may be some subtle palette tricks that I am unaware of and thus don't cover. Palettes are tricky to use though, so be certain to approach any experimentation with lots of patience, otherwise you may well be driven insane. One final note before we begin: This article makes the assumption that our application will display a 256 color image in our application's window, and use the DIVE API to do it. DIVE is capable of displaying images with more than 256 colors, but for simplicity's sake this series of articles will not cover those situations. What is a Palette?A palette is an array that tells PM what colors to use when displaying an image. A 256 color bitmapped image has one byte for each pixel. This byte tells what color in which to display that pixel. A palette is used to convert that byte into the color information the display hardware needs to actually put the color on the monitor. Palettes can have up to 256 entries. Each entry describes one color, and the color information is stored in three bytes, one each for the component colors red, blue, and green. As they are stored in bytes, each of these component colors can have a value ranging from 0 to 255. Using varying amounts of these three colors, any other color can be described. A bright red would have Red set to 255 and both Green and Blue set to zero. Black is achieved when all three are set to zero and white results from all three being set to 255. All palette activities for an OS/2 program are handled through the palette manager. The palette manager is part of the OS/2 API, and is accessed through the API function calls. A palette manager is needed in a GUI environment because multiple programs may be running at the same time and each program may require different color needs. The palette manager tries it's best to meet these needs. However, the palette manager is restricted by hardware limitations. Many common color modes of video cards have a maximum of being able to display 256 colors at the same time. Although each of these colors may be selected from among 262,144 colors (256k colors), on the graphics card there is a physical maximum of 256 color registers to hold the colors. This has led to the development of two different kinds of palettes: physical and logical. The physical palette is the palette set in these hardware registers. Under OS/2, you can not set the physical palette directly. The palette manager will always determine what colors go in the physical palette, and in what indices these colors are placed. You can read the physical palette with GpiQueryRealColors(), but you cannot set it. [Note: There is one exception to the above. In dive.h a comment explains a way to set the physical palette with undocumented calls. I would urge everyone to avoid this method though, since it is unknown what that code would do in a non-paletted video environment (such as the 32 and 64 bit modes), and this method may not be supported on different hardware (such as the PowerPC, or future Intel based hardware). I have not experimented with the undocumented code in dive.h because of lack of information on the undocumented calls, and because I have been able to achieve the results I need through the palette manager and without messing up the integrity of the desktop.] Although programs cannot access the physical palettes, they can have their own logical palettes. A logical palette is set up in the same format as a physical palette. The logical palette is sent to the palette manager, and whenever your window has focus, the palette manager will change the physical palette to match the logical palette as much as possible. For the colors that do not match the physical palette exactly, the palette manager will decide the closest match, and use that instead. When you request that color 4 be drawn to the screen, the palette manager will look at color 4 in the logical palette, decide what color it most closely matches in the physical palette, and send the new color out to the display hardware. By approaching palettes this way, several applications can share the scarce palette entries at the same time. The palette manager will always try and fulfill the needs of the palette in the active window. Applications whose windows are not active will still have their colors remapped to the closest palette entry, but the palette manager will not use the inactive palettes to determine the contents of the physical palette. Note that remapping is done when an image is drawn to the screen, such as with DiveBlitImage(). If the display is in a 256 color mode, the physical palette changes, and a window is not redrawn, then its colors will be wrong. But what about graphics modes with more than 256 colors? Many users of OS/2 have their displays set to modes that offer 32k, 64k, or even 16.4 million colors. In these cases, there are no color registers to fill. If two applications request palettes of 256 different colors each, both requests can be met without any problem. In these cases there is no physical palette to set, but a logical palette is still required. The system must know what color each of the 256 palette entries in our source bitmap is to be displayed as when it converts each pixel to a 15, 16, or 24 bit color value. How to Use Palettes Under OS/2Our goal is to use palettes in conjunction with the DIVE API in order to display images in a window. Although DIVE has the calls DiveSetSourcePalette() and DiveSetDestinationPalette(), neither of these calls affects the physical palette in any way. These calls are used to tell DIVE what logical palette it is mapping from and what physical palette it is mapping to. To change the physical palette in 256 color video modes, you must request that the palette manager remap the physical palette as much as possible to a specified logical palette. The steps for the creation and activation of a palette are as follows:
NotesCertain things must be considered in each of the above steps. The important ones are detailed below, and the general implementation of the steps can be viewed in the sample files game2.cpp and os2pal.cpp.You can find them in code.zip Step 1: WinOpenWindowDC() will only work once per window. Multiple calls will fail, so be certain to save the returned device context handle. Step 2: again, make certain that you do not use a cached micro PS. For our purposes, we will call GpiCreatePS() with the flags PU_PELS, GPIF_DEFAULT, GPIT_MICRO, and GPIA_ASSOC. Also, be certain to save the handle to this presentation space and use it throughout your program. This includes using it with the DIVE code that calls GpiCreateRegion(), GpiQueryRegionRects(), and GpiDestroyRegion(). If you create and use separate PSs for palette and DIVE tasks, then in 256 color video modes your blitted images will appear black, even though they will blit correctly in higher color resolution modes. Step 3: the array that describes your palette is an array of unsigned longs. Three of the bytes in the ULONG describe the red, blue, and green components of the color, and the fourth byte is a flag byte. The flag byte tells the palette manager a bit more information about that specific color.For our purposes, we will set all of the flags to the same value. This value is PC_RESERVED, and it signals the palette manager that the color in/question may change frequently due to palette animation. Therefore the palette manger will not use these colors for other windows, and you have more control over what colors are put in the physical palette while your window has focus. The formula for placing the four component bytes into a ULONG is: ulArrayEntry = ((ULONG)byFlag << 24) + ((ULONG)byRed << 16) + ((ULONG)byGreen << 8) + byBlue; [Editor's note - you can also use the RGB2 structure for this purpose.] Step 4: with GpiCreatePalette(), for flOptions we use LCOL_PURECOLOR to prevent dithering, but we do not use LCOL_OVERRIDE_DEFAULT_COLORS in order to keep from messing up the desktop colors. For ulFormat we use LCOLF_CONSECRGB as it is currently the only supported format. We pass this function an array of the format described in step 3 above. Step 5: presentation spaces are used to hold display information for a window. If we were to use Gpi drawing routines, the PS would hold the current drawing colors, line characteristics, marker styles, character attributes, and so on. The PS also holds palette information for our window. To tell the PS which palette to use, we call GpiSelectPalette(). Step 6: even though we selected a palette to use for our PS in step 5, this palette is not yet made active. In order to tell the system to use our palette whenever our window has focus, we must call WinRealizePalette(). Step 7:we must tell DIVE which color each byte in our DIVE buffer stands for. This is done with DiveSetSourcePalette(). If DiveSetSourcePalette() is not called, then the default palette is used. If your DIVE buffer does not use the default palette, your results will be unpredictable. Step 8: we must also tell DIVE about the colors of the display hardware. This can be done by retrieving the physical palette with GpiQueryRealColors() and passing it to DiveSetDestinationPalette(). A shortcut to this is to call DiveSetDestinationPalette() with a null pointer to the palette array. This will instruct the routine to call GpiQueryRealColors() on its own. he notes for this step also apply to step 10 and the WM_REALIZEPALETTE message. Steps 11 and 12: When our game loses focus, one of the things we want to do is give the system back as many colors as possible so that other applications may use them. Since we use the PC_RESERVED flag with all colors of our game's palettes, our palette holds a lot of weight with the palette manager. By deactivating our palette when we lose focus, the window receiving focus will have its colors more readily remapped into the physical palette. By specifically reactivating our palette when we regain focus, our colors are set in the physical palette without delay. We need to redraw our window when we regain focus in case we received focus when another overlapping window closed. Such situations lead to a corrupted display if we don't redraw our window. In the future when we have a blit thread continually drawing our image to the window, the explicit redraw will not be needed. The OS2Palette objectMany of the above steps can be combined because they always occur after one another, such as GpiSelectPalette() and WinRealizePalette(). Also, we may have to work with palettes other than OS/2 palettes. An example of this are the palettes stored in compressed bitmap files. To simplify palette operations we will use a palette object that hides from us repetitive tasks. For learning about the specific implementation of any of the tasks provided by the palette object, I encourage you to look at the source code for the object in os2pal.cpp in code.zip. The following functions are those provided by the OS2Palette object. They do not cover every single aspect of palettes, but they provide an illustration of the major points. Also, they provide all the functionality we need for our simple game.
Palettes for Multiple Child WindowsSometimes I am very glad that I don't keep a rocket launcher on hand near my computer. Recently I'm afraid that I would have had to blow my computer into tiny, unrecognizable pieces considering how much grief it caused me. After months of being bothered by the same, nagging palette problem, I finally discovered a solution. The problem arises when you have two child windows, both of which are using DIVE to blit an image to its client area. I ran across this situation while I was writing editors and other utilities for my own game, though such a situation could very well arise in a game with multiple windows. In order to get the same palette to display correctly in both windows, implement a palette as above in the "How To Use Palettes in OS/2" section. The various steps will be split amongst the parent and children windows as follows:
Performing the above steps should allow the same palette to be used for two separate child windows. Graphics FormatsWhen graphics are stored to disk, they are usually saved in a special format rather than just dumping memory to a disk file. These formats contain additional information about the graphic such as its size, number of colors, palettes, and other types of info. The graphic data is also usually compressed in order to save disk space. Compression/decompression algorithms are usually called codec's, and each graphic format usually has it's own codec which is different from the others. Codecs differ in their compression ratio, speed, and quality. Certain codecs throw out some of the original information during compression, and then try and guess what is missing when they decompress the image. These are called lossy codecs because they lose some of the data. JPEG is an example of a format with a lossy codec. Lossless codecs generate the same image upon decompression as was given to them for compression. Popular formats with lossless codecs include GIF, PCX, TIF, and PNG. Our code will deal with PCX files. PCX files have the advantage of being supported natively by many paint programs both shareware and commercial. This format was originally developed by ZSoft for their paint program PC Paintbrush, but it was adopted by many other programs as well due to its ease of use. PCX files support monochrome, 16-color, and 256 color images though we will only concern ourselves with the monochrome and 256 color capabilities. PCX files are also good because their compression scheme is very simple to understand. It is not a compression scheme that works well for all graphics though, and many scanned images would wind up larger than where they started if the PCX scheme were used on them. Nevertheless, it will suit our needs nicely. As far as implementation in our sample code is concerned, the PCX encoding/decoding object is derived from a parent class called BitmapPainter. This allows us to treat different graphic formats in the same way. The same calls are used to setup, compress, and decompress graphics no matter what format in which they are stored. Although we will only implement code to deal with PCX files, you could write code to support other formats without having to change much code in programs that already use BitmapPainter-derived objects. Also, our PCX display code will use the Canvas class to hold the decompressed image. The Canvas class was covered in the first article of this series. On to PCX!PCX files have three parts: the header, the image data, and the palette. Monochrome images only have a header and the image data, since all pixels are either black or white. The HeaderThe header of the PCX looks like this: typedef struct { BYTE byManufacturer; // always 0x0a BYTE byVersion; // version number BYTE byEncoding; // always 1 BYTE byBitsPerPixel; // color bit depth USHORT usXmin; // coordinate of left side of image USHORT usYmin; // coordinate of top of image USHORT usXmax; // coordinate of right side of image USHORT usYmax; // coordinate of bottom of image USHORT usHres; // horizontal resolution of creation device USHORT usVres; // vertical resolution of creation device BYTE abyPalette [48]; // color palette for 16 color images BYTE byReserved; BYTE byColorPlanes; // number of color planes in the image USHORT usBytesPerLine; // line buffer size USHORT usPaletteType; // gray or color palette BYTE abyFiller [58]; } PCXHEADER; The header is exactly 128 bytes long, and is always located at the very beginning of a PCX file. A more detailed meaning of each field is as follows:
The CodecThe algorithm for decoding a PCX image is simple. You take the current byte of the image data and look at it's top two bits. If they are both clear, then you write that byte out to your canvas. If they are both set, you clear them and that byte becomes your duplication count, or run length. Take the next byte and copy it to the canvas "run length" number of times. Repeat the process until you have enough bytes to fill a row. Move to the next row down and repeat the process for that row. Do this until there are no more rows left in the image. The only catch to the above algorithm is when you run across a single pixel that has information in one or both of the top two bits. Bytes for these types of pixels are handled as having a run length of 1, since the entire 8 bits of the byte can be stored in the byte following the run length byte. Encoding a PCX is just the reverse of this process. Start at the first pixel in the top row of your image. Count how many times this pixel color appears in a run. Do not let this run length go over 63, because you can only store the run length in 6 bits (the top two will be set). If the run of bytes is greater than one, set the top two bits of the run length and send it to your encode buffer. Next send the byte to duplicate. If there is only one byte, check and see if any of the top two bits are set. If one or both are set, output a run length of one, and then output the byte. Otherwise, simply output the byte. Repeat until you are done with a row, and then move to the next row down. Continue in this way until all rows are done. This same codec is used in both monochrome and 256 color images, though the pixels are stored slightly differently. For monochrome images, each byte contains 8 pixels, since only one bit is needed for each pixel. If the bit is set, it denotes a white pixel. If it is clear, it denotes a black pixel. The most significant bit is the left most pixel in that byte, and the least significant pixel is to the right. Bits can be tested in the byte with a bitwise "and" operation. 256 color images have their bits stored right next to each other. Since it takes 8 bits per pixel, the entire byte is used for the color index. You can decode the row directly onto the surface of a Canvas. The PaletteIn monochrome images, there is no palette because the pixels can either be white or black. In 256 color images, the palette is attached to the end of the PCX file. The palette is stored as one byte each for the red, green, and blue components of each color, in that order (RGB). There are 256 colors possible, so the palette is 3 color bytes * 256 indexes = 768 bytes long. To make sure that you aren't reading image data instead of palette data, PCX files have the byte 0x0c right before the palette information starts. To read a PCX palette you find the end of the file and move back 769 bytes. Check that byte to see if it equals 0x0c. If it does, read the next 768 bytes as your palette. Byte OrderOne last note about palettes: To use the palette attached to the PCX file, you will need to convert it to an OS/2 palette. An OS/2 palette has the elements Flag-Red-Green-Blue stored in a ULONG. However, Intel based PCs store USHORTs and ULONGs in a byte reversed order, and if you access the OS/2 palette as an array of bytes instead of an array of ULONGs, then you must take this into consideration. The byte reversal scheme works like this: Suppose you have a variable at memory location 0x1000. A byte would be stored at location 0x1000, a USHORT would be in locations 0x1000 and 0x1001, and a ULONG would be stored in locations 0x1000 through 0x1003. Placing a byte is no problem; just put it in location 0x1000. A USHORT is made up of two bytes. One byte handles the lowest 8 bits, and the other byte handles the highest 8-bits. When an Intel based PC stores the USHORT in memory, it will place the lowest 8 bits in the first byte (0x1000), and the highest 8 bytes in the next byte (0x1001). A number under 256, say the value 42, would be stored in the lowest 8 bits of the USHORT. A pointer to our USHORT would have the value of 0x1000. When the USHORT is accessed with the pointer, then the bytes will be placed in their correct order so that the value of 42 is retrieved. However, what if we typecast this pointer and use it as a pointer to type BYTE. A BYTE can hold the value of 42, but it is only looking for a single byte in memory (8-bits). Since the bytes are reversed and the lowest byte is at location 0x1000, the byte pointer can read the value fine. If the bytes were not reversed, then the value at 0x1000 would hold a zero and the byte pointer would receive the wrong value. This is the rationale behind the reversed byte order scheme. For this same reason, the bytes that make up a ULONG are reversed twice. You can look at ULONGs like two USHORTs, but the USHORTs are reversed so that the lowest 8 bits of the ULONG are placed at the first memory location (0x1000). What all this means for palettes is this: You access the bytes in a PCX one after another in the order of Red, Green, Blue. byRed = *(pbyColor + 0); byGreen = *(pbyColor + 1); byBlue = *(pbyColor + 2); If accessed with a byte pointer, the bytes in an OS/2 palette are accessed in the order Blue, Green, Red, Flag. byBlue = *(pbyColor + 0); byGreen = *(pbyColor + 1); byRed = *(pbyColor + 2); byFlag = *(pbyColor + 3); For implementation of a PCX decoder and encoder, check out the sample source code that comes with this article. Handling the header, codec, and palette can all be seen in pcx.cpp. The handling of the byte reversal scheme can be seen in the Convert() routine of os2pal.cpp. The PcxPainter objectThe PcxPainter object is derived from the BitmapPainter object. All of the member functions called by a program are virtual functions in the BitmapPainter object, so you could just as easily replace the PcxPainter object with a GifPainter or TifPainter, and not have to change much of anything else in your program. The functions of the BitmapPainter object are:
Conclusion and News of the FutureWell, that's about it for my second article in Gearing Up For Games. As always, comments, flames, and questions are welcomed. I can be reached at the email address listed in the "Contributors" section.Next time I will probably look into threads, semaphores, timers and other multitasking concerns. If I have time, I may start on how to draw sprites as well. In the future, I will be looking into the fullscreen support aspects of DIVE. I recently added fullscreen support to a project I'm working on, and it was a fairly simple process. It took about 15 minutes to write all the code needed to implement fullscreen support, an hour to get the darn thing to compile (problems with parameter passing since header files for fullscreen support don't exist yet), and another hour working out undocumented pitfalls of fullscreen modes. I'll be certain to give a full report in a couple of months as availability of the Games SDK draws closer. It looks like the Games SDK will be released through the DevCon CD subscription program. For those of you unfamiliar with DevCon, it is subscription that comes four times a year, and each "issue" contains several CDs with information, toolkits, and sample code on them, as well as a paper newsletter with articles on various aspects of developing for OS/2. I included ordering information in my last article, so I won't repeat it here. I don't know if the Games SDK will be released through other means other than DevCon. I think that IBM should release it freely, but that is just my (and a lot of other people's) opinion. Also, I have recently learned that IBM has released joystick drivers for OS/2. The drivers can be obtained by anonymous ftp to ftp01.ny.us.ibm.net in the /pub/games directory. The filename is JOYSTICK.ZIP. I haven't found any programming information for these drivers yet, but I'll be sure to fill everyone in on what I can find as soon as I find it! Whoops!Well, looks like I made a few mistakes in the code for my last article. Time to come clean on them:
Compiling the Sample Code With the Watcom CompilerI did not give instructions on how to get the sample code to compile with the Watcom compiler (which I actually use by the way.) To use the DevCon toolkit with Watcom, the following things must be done: In Your C/CPP CompilationsFirst, for all of your C and CPP files that use the DevCon Warp Toolkit, add: #define __IBMC__ before you include any of the DevCon header files. Note that there are two underscores both before and after the "IBMC", not just one. Also make sure that the compiler is looking in the \H directory of your DevCon toolkit for header files, not in the WATCOM\H\OS2 directory. In The File mmioos2.hFind the section titled : "Country codes (CC), languages (LC), and dialects (DC)." You will notice that some of the defines in this section begin with a zero. This causes the Watcom compiler to read these numerals as octals (as any good ANSI compiler would <grin>). Remove these zeroes. Therefore the definition: #define MMIO_CC_USA 001 would become #define MMIO_CC_USA 1 This needs to be done from MMIO_CC_NONE (value of 0) to MMIO_CC_TURKEY (value of 90). In Your Linker setupDon't forget to include the library mmpm2.lib when you link. This can either be done in the IDE in the menu "Options \ OS/2 Linking Switches \2.Import, Export and Library Switches" in the "Libraries(,):[libr]" window, or in the command line of your linker with the statement "libr mmpm2.lib" (without the double quotes, of course). Also, be certain to have a large enough stack for your program. The default stack for the Watcom IDE is about 8K I think. The MMPM/2 documentation suggests at least a 16Kb stack, but I compile with a 32K stack.
|