VESA standardizes the software interface to many "super" VGA cards, but nobody said it made programming effortless.
When I first started coding in BASIC on the Apple IIe many years ago, I made a fantasy adventure game called "Fubar's Jubilant Sword," or something. I sat down with a sheet of engineering paper and a pen, marked off the screen dimensions on the page, sketched the graphic for my title screen, and then diligently mapped every little square of the grid that had ink on it to a pixel array in my program -- my very first bitmap.
Things change. Today, a standard VGA-equipped IBM PC can display 256 colors simultaneously, selected from a palette of 16.8 million. It can also achieve a 640x480 pixel screen resolution -- but not while displaying 256 colors. Thus, the standard VGA is a tantalizing beast: if it could only give both color and resolution at once, it could display lifelike images on a 15-inch monitor. But it can't.
Fortunately, a host of "Super" VGA cards appeared on the scene, with resolutions of 640x480x256, 800x600x256, and beyond. Photorealism on a PC was no longer a fantasy. But these new cards, unlike the IBM-standard VGA, varied from one manufacturer to the next. Writing a program with Super VGA graphics meant writing system-level code for every make of video card your software was likely to encounter.
Someone needed to set a standard for SVGA cards, so the Video Electronics Standards Association (VESA) was formed in 1989 to write one. They created the VESA BIOS Extension (VBE), of which the two major releases are VBE 1.2 (1991) and VBE 2.0 (1994). The VBE specifies that manufacturers bundle their cards with driver software that extends the existing BIOS of an IBM PC. This driver provides a set of VESA-defined extended video BIOS system calls to isolate the programmer from the manufacturer-dependent quirks of the hardware. With the VBE, you have a single interface that works no matter who made the video card.
As the VBE catches on, more and more commercial programs using 640x480x256 display resolutions and higher are appearing on the shelves, awing the public with their photorealistic images and surrealistic renderings. Forget those boring 16-color user interfaces and ugly 320x200 games; I'm going to give you a peek under the hood and see how the VBE works, finishing up with a program that will display a photorealistic image on the screen.
Talking To The VBE
As the term BIOS extension implies, programs access the VBE functions through the IBM PC's BIOS video interface. They make a PC/MS-DOS system call to interrupt 0x10 with the CPU's AH register set to 0x4f, the AL register set to the VBE function number, and any other registers loaded according to the function's requirements. Most of the VBE.C functions ( Listing 1) are little more than shells over these system calls. Figure 1 shows the core function list.
The VBE will return the value 0x004f in the AX register if all went well. Any value other than 0x4f in the AL register means the driver doesn't support the function. AH = 0 represents success, AH = 1 is failure, AH = 2 means the hardware doesn't support the function (VBE 2.0+ only), and AH = 3 means the function is invalid in the current video mode (also VBE 2.0+ only).
Detecting The VESA VBE
Function 0 (Return VBE Controller Information) is the first function any VESA-aware application should call. The application calls this function to see if the VESA driver is present, which version it is, what modes are available, and so on. The C code to call this function looks like this:
union REGS r; struct SREGS s; r.x.ax = 0x4f00; r.x.di = FP_OFF(VbeInfo); s.es = FP_SEG(VbeInfo); int86x(0x10,&r,&r,&s);(The int86x function, or its equivalent, is supplied with PC C compilers to enable programs to call the MS-DOS interrupt functions from C. The REGS and SREGS structures are defined in DOS.H to hold copies of the 80x86 register contents.)
VbeInfo must point to a free block of memory at least 256 bytes long; it must be 512 bytes long if you want the 2.0 extended information as well. The function will treat the block of memory like a VbeInfo_t structure as shown in VESA.H (Listing 2) .To get the function to return the extended fields, initialize the VbeSignature field to the characters "VBE2" before calling the VBE function
When the call returns, the AX register should equal 0x004f, indicating success, and the VbeSignature field should contain the characters "VESA." There are a few other important fields to know in this structure:
The VBE.C function VbeGetVersion forms a wrapper over this system call, returning a Boolean on whether or not the VBE was detected.
- VbeVersion: This is a BCD (binary coded decimal) value representing the VESA version of the VBE. 0x102 is version 1.2, 0x200 is 2.0, and in general, (unsigned)VbeVersion>>8 yields the major version number, and VbeVersion&0x00ff yields the minor version number.
- VideoModePtr: This is a far (32-bit) pointer to a 16-bit integer array containing the numbers of all the VESA-defined SVGA video modes this driver supports. The last element in the list is always -1 (0xffff).
- TotalMemory: This function sets this field to the number of 64Kb blocks of memory installed on the video card. For instance, if you see a 0x10 in this field, you have 16*64Kb = 16*65,536 = 1Mb of video memory installed. TotalMemory<<6 will yield the memory in Kb, and (unsigned)TotalMemory>>4 yields the memory in Mb.
- OemStringPtr: Upon return this will point to an ASCIIZ (null-terminated) string containing hello-world information about the VBE manufacturer.
Detecting SVGA Mode Support
The previous function indicated which modes the VGA board would support by setting the VideoModePtr field to point to an array of supported mode numbers. But after obtaining this mode list from the previous function, the application still must check to see if a particular mode from the list really can be used. Function 1 (Return VBE Mode Information) answers this question. The call looks like this:
r.x.ax=0x4f01; r.x.cx=Mode; r.x.di=FP_OFF(ModeInfo); s.es=FP_SEG(ModeInfo); int86x(0x10,&r,&r,&s);The Mode passed into the CX register is one of the mode numbers from the array of supported modes. Figure 2 lists the VESA-defined modes. As with function 0, the memory block pointed to by ModeInfo must be at least 256 bytes long. The VBE function will treat this block as a ModeInfo_t structure. (This structure is not shown here, but is available electronically. See p.3 for details.) There's a lot of information in this structure, but here are some of the most important fields:
- int ModeAttributes: Bit 0 (LSB) is set if the requested mode is available, and clear if the hardware doesn't support the mode. Bit 3 is set for color modes, clear for monochrome.
- int Xresolution, Yresolution: These fields indicate the screen resolution in pixels (graphics modes) or character cells (text modes).
- char BitsPerPixel: This field gives the number of bits used to represent a single pixel, or the color resolution of the mode. 1<<BitsPerPixel yields the number of colors that can be displayed simultaneously on the screen: you get 16 colors with four bits, 256 colors with eight bits, 16.8 million with 24, and so on.
- char MemoryModel: The memory model of the mode refers to the way the video segment is mapped to the display; more on this later.
- int BytesPerScanline: This represents the length of a logical scan line in display memory. (Note that this may or may not be larger than what Xresolution may imply!)
- int WinASegment, WinBSegment: These fields tell which memory segment(s) have been mapped to the video display area.
- int WinGranularity, WinSize: These show the size and granularity of the video segment -- subjects I'll also tackle later on.
Here's a typical example of how a VESA mode can turn up on the mode list but not actually be available: your VBE may support a mode like 1,280x1,024x256 (most do), but this mode needs one byte/pixel * 1,280 pixels * 1,024 pixels = 1,310,720 bytes, or 1.25 Mb of display memory (1Mb = 1,048,576 bytes). If your card has only has 1Mb video memory installed, the ModeAttributes bit 0 will be clear because you don't have enough memory to run the mode.
The code disk and online services contain a program CHECK.C that uses the VBE.C functions VbeGetVersion and VbeCheckMode to return information about the VBE on the current machine, and then to report on each of the VESA modes supported by the driver.
Initializing the VESA Modes
Now that we've determined that the mode we want to use is available, we can use function 2 (Set VBE Mode) to initialize the mode. Calling function 2 is accomplished as follows:
r.x.ax=0x4f02; r.x.bx=Mode; int86(0x10,&r,&r);This is what the VESA.C function VbeSetMode does. This function can be used to set any of the standard VGA modes as well. If Mode == 3, for example, the code above would restore the computer to text mode.
You get a couple of extras with the VBE function: if you pass Mode|0x8000 into the BX register, you can set the mode without clearing display memory (and the screen); and (for VBE 2.0+ only) if you pass Mode|0x4000 you can initialize the mode to use the linear frame buffer memory model.
Of Memory Models and Windows
Okay, we can detect the driver, check the video modes, and initialize them; time to plot some high-res SVGA pixels. How, exactly, does a program plot hi-res SVGA pixels? The answer, unfortunately, is "it depends." The modes do have one thing in common: each of them map the display to a "window" in main memory; but there are numerous ways of organizing that map. In other words, different video modes use different memory models.
In this article I'll deal with the most common SVGA mode, 0x101 (640x480x256). This mode uses an 8-bit packed pixel memory model, a fairly easy layout to deal with. Display memory begins at the start of the memory window (typically located at A000:0000), with each consecutive byte mapping to each consecutive pixel, going from left to right, top to bottom. The value contained in the byte is the color number for the pixel. These color numbers reference a palette that can contain 256 entries (the number of possible values in a byte). This is where the term "eight-bit color" comes from -- eight bits is one byte.
Other common memory models are as follows: planar is a counterintuitive layout used by VGA mode 0x12; non-chain 4 256-color (or planar 256) is an even more counterintuitive scheme used in VGA mode X; direct color bypasses the palette and writes the color data itself to the memory window, and is used by 15-, 16-, 24-bit color modes, and up.
The ModeInfo block's MemoryModel field tells which memory model the specified mode uses. Most SVGA graphics modes return either 4 (packed pixel) or 6 (direct color) in this field.
Packed pixel is almost as easy as it gets, but that's a relative assessment: there's still a lot to think about. Remember the ModeInfo fields, WinASegment and WinBSegment? These fields indicate what segment or segments of main memory have been mapped to the display. Typically, WinASegment == 0xa000, the standard video segment for color graphics modes on the IBM PC. In the packed pixel memory model (for mode 0x101), each byte in the video segment corresponds to one pixel on the screen, beginning with pixel (0,0) in the upper-left corner. In general, pixel (x,y) and its corresponding memory offset are related as follows:
Offset(x,y) = Xresolution*y + x x(Offset) = Offset mod Xresolution y(Offset) = Offset div XresolutionWhere Xresolution == 640 in mode 0x101. On an IBM PC, there are 65,536 bytes in any segment, the last one at offset 65,535. But:
x(0xffff) = 65,535 mod 640 = 255 y(0xffff) = 65,535 div 640 = 102So you can only plot pixels from (0,0) to (255,102) before overrunning the video segment, even though the mode 0x101 screen goes all the way to (639,479). To plot pixels beyond (255,102) the memory window must be remapped to a different part of the video display area.
The function that remaps the display is function 5 (VBE Display Window Control), subfunction BH = 0, Set Memory Window. (Subfunction BH = 1 fetches the current window position in the DX register.) Here's how you call function 5:
r.x.ax=0x4f05; r.h.bh=0; r.h.bl=Window; r.x.dx=Position; int86(0x10,&r,&r);The BL register tells the VBE which window to remap: BL = 0 means window A, and BL = 1 means window B. The window position passed into the DX register must be in WinGranularity units. WinGranularity is the smallest increment (in Kb) by which a memory window can be shifted. If WinGranularity == 64, then the memory window can start at the first pixel or the 65,536th, but not between them.
The ugly part of all this is that when we move the window from position 0 to position 1, offset 0 in the video segment now corresponds to pixel (256,102), which isn't even the first pixel in the scan line. We need to rethink the relationship between pixel coordinates, window positions, and memory offsets. It comes down to this:
Offset(x,y) = (x+y*Xresolution) mod WinGranularity Position(x,y) = (x+y*Xresolution) div WinGranularity x(Offset,Position) = (Position*WinGranularity+Offset) mod Xresolution y(Offset,Position) = (Position*WinGranularity+Offset) div XresolutionAssuming Xresolution == 640, WinASize == 64Kb, and WinGranularity == 64Kb, then it takes about four and a half windows to cover the whole screen:
Position 0: (0,0) to (255,102) Position 1: (256,102) to (511,204) Position 2: (512,204) to (127,307) Position 3: (128,307) to (383,409) Position 4: (384,409) to (639,479)One more thing: the ModeInfo block contains a field called WinFuncPtr. WinFuncPtr is a far pointer to the window control function (function 5) just discussed; it's there because calling it this way is faster than calling the function through the VBE interface. Because its arguments are passed in the CPU registers and not pushed on the stack, it must be called from the inline assembler:
_asm { sub bh,bh ;subfunction bh=0: set memory window mov bl,Window ;bl=window a or b mov dx,Position ;dx=position in WinGranularity units call WinFuncPtr ;call window control function directly }Note that this code didn't have to load the AX register the VBE function number. We don't need it here because WinFuncPtr already references function 5 directly. The VESA.C function VbeSetWindow uses the interrupt version for clarity, but when speed counts, use the direct call instead.
Setting the DAC Palette Registers
I mentioned earlier that in an eight-bit, packed pixel mode like VESA mode 0x101, the value of the byte written to memory dictates the color of the pixel on the screen. What happens, roughly, is this: this byte gets fed to the video card's DAC (digital-to-analog converter) chip, which uses it to index a table of 256 RGB triplets (the red, green and blue components that actually make up the color). The chip converts this data to an analog signal that the video monitor can use.
This table, called the DAC color register set or the 256-color palette, can be and frequently is preloaded with a custom color map before anything is drawn to the screen. A photographic-quality 256-color image achieves the smooth gradations in hue and shadow by loading all 256 palette entries with the colors it needs most. A program can load the palette through the VBE or through the BIOS. Since VBE function 9 (Load/Unload Palette Data) only exists in VESA version 2.0+, I'll mention both methods. There isn't much difference between them anyway.
The code to load Count palette registers beginning at register offset First looks like this, using function 9:
r.x.ax=0x4f09; r.h.bl=0; r.x.cx=Count; r.x.dx=First; r.x.di=FP_OFF(PaletteData); s.es=FP_SEG(PaletteData); int86x(0x10,&r,&r,&s);The BL register specifies subfunction 0, Load Palette Data. The PaletteData array needs to be at least 3*Count bytes long, since the data for each entry is a triplet of red, green, and blue components, in that order. If you set all 256 palette entries, Count == 256, First == 0, and sizeof(PaletteData) == 768.
The standard BIOS version of loading the palette uses video function 0x1012:
r.x.ax=0x1012; r.x.bx=First; r.x.cx=Count; r.x.dx=FP_OFF(PaletteData); s.es=FP_SEG(PaletteData); int86x(0x10,&r,&r,&s);So the VBE and standard BIOS techniques are pretty close. The VESA.C function VbeSetPalette will use the VBE call under 2.0, and the BIOS call under prior versions.
The Moment of Truth
Check out PCX.C (Listing 3) . This (very) scaled-down program uses the VESA.C functions to display a 640x480x256 (mode 0x101) PCX image file. I've extracted this code out of a more comprehensive PCX file viewer I wrote that handled CGA, EGA, MCGA, VGA, as well as eight-, 16-, and 24-bit SVGA pictures. I've tried to make this example as small and concise as I could, so this program will only handle PCX files that can be displayed in mode 0x101.
A quick word about the PCX file format: it was developed by Zsoft Corporation for their PC Paintbrush software, and was a de facto standard for a long time. Today it's being displaced by the GIF (which offers better 256-color compression) and JPEG (which offers excellent true color compression) standards. Though PCX is a poor choice of formats for SVGA images, it is the simplest.
A PCX file begins with a header record, which I've typedef'd as pcx_t. I've abridged the structure of the block a bit to make it as self-explanatory as possible; essentially, the PCX file will have Manufacturer == 10 (i.e., a PCX file), Encoding == 1 (run-length encoded), Planes == 1 (packed pixel), BitsPerPixel == 8 (256 colors), Xmax-Xmin<640, and Ymax-Ymin<480 (no larger than 640x480 pixels). The PCX.C function ReadHeader checks for these values in the header to ensure the viewer can display the requested file.
If the header's Version field is 5, a 256-color custom palette resides at the end of the file. (Just about every 256-color PCX file will have its own palette). The palette record consists of the signature byte 0x0c followed by a 768-byte block of palette RGB data. This block is similar to what gets passed into VBE function 9 (LoadPaletteData), with one exception: the DAC uses only 6-bit RGB components, and the PCX palette contains 8-bit components. To convert from a PCX palette component to a DAC palette component, the viewer just drops the two least significant bits with a right shift. The PCX.C function ReadPalette shows how to read and translate the palette data.
The compressed image lies between the header and the palette data. Decoding the data is simple: fetch a byte and check the two most significant bits. If they're not both set, then the byte represents a single pixel value. If both bits are set, then the low six bits represent the number of following consecutive pixels that take the value of the next byte fetched. This is a simple form of run length encoding. The PCX.C function ReadLine demonstrates the decoding algorithm.
The viewer's logic is pretty straightforward. The program opens the requested file, verifies the header, and translates the palette data. Then it sets the video mode and loads the palette into the DAC. Finally, the viewer enters the read/display loop, which decompresses one line of the image into a buffer and then copies the buffer to the display. When the viewer is done, it uses the VBE to set the video mode back to text mode (VGA mode 3). The code is not meant to be terribly efficient, just illustrative. (When I yanked the code from my file viewer I did a lot of de-optimizing in favor of simplicity.)
Is That All?
Heck, no. I've ignored a bunch of stuff in order to nail down the basics without writing a book. There's plenty of territory left to explore: linear frame buffers and DPMI; other memory models and color models; multiple window segments, scrolling displays, and page flipping...you get the idea.
Graphics technology is a fleet-footed beast. Somewhere in my garage, buried at the bottom of a disintegrating cardboard box, on a crumbling sheet of engineering paper, is my very first bitmap. Big pixels. No color. But back then it added a touch of class to "Fubar's Jubilant Sword."
Or whatever the game was called. It's been about 10,000 years since then. o
Standards Information
Programmer's Toolkit: VBE/Core 2.0. Video Electronics Standards Association. This is the most current version of the official VBE standard, can be purchased from VESA (Voice: 408-435-0333).
Zsoft PCX File Format Technical Reference Manual, Revision 5. Zsoft Corporation. This is available from Zsoft's BBS at 404-427-1045 (9600/2400,N81). Also available from Zsoft is the file SHOW_PCX.PAS, a Pascal program that displays CGA, EGA, MCGA, and VGA files.
Chris Krehbiel is a systems programmer with Logicon Syscon Corporation. He has a degree in computer science from Thomas Edison State College, and lives in Williamburg, Virginia. He tinkers with games and graphics and can be reached at ckrehbiel@logicon.com.