[an error occurred while processing this directive]
The standard VGA modes are all very well, but before long you are likely to want a higher resolution, or perhaps one with more than 256 colors. This means switching to an SVGA mode, which stands for Super-VGA, ie. better than what was possible on the original VGA board. There are many hundreds of different and incompatible SVGA cards on the market today, but you won't need to write special code for each one because you can use the standard VESA interface instead. This is a software API that was designed by the Video Electronics Standards Association, and is usually implemented either as a loadable TSR utility or as part of the BIOS ROM on your video card. VESA allows you to do things like setting graphics modes and displaying images without needing to know the hardware details for every graphics chipset: this is obviously a good thing if you want your program to work across a range of different machines!
If you don't already have this, you should start by getting hold of a copy of the VESA spec (see the links at the end of this file). A lot of material from the official documentation will be repeated here, but this is a beginners tutorial rather than a complete technical reference so you will probably want to have the spec at hand for reference.
All the VESA functions are accessed by calling int 0x10 with a value of the form 0x4F?? in the AX register, where ?? represents the specific function that you want to execute. They will return a value of zero in AH if the call succeeds, or non-zero if there was an error.
The first step is to make sure that a VESA driver is present, and to retrieve a copy of the VESA information structure. This is defined as:
typedef struct VESA_INFO { unsigned char VESASignature[4] __attribute__ ((packed)); unsigned short VESAVersion __attribute__ ((packed)); unsigned long OEMStringPtr __attribute__ ((packed)); unsigned char Capabilities[4] __attribute__ ((packed)); unsigned long VideoModePtr __attribute__ ((packed)); unsigned short TotalMemory __attribute__ ((packed)); unsigned short OemSoftwareRev __attribute__ ((packed)); unsigned long OemVendorNamePtr __attribute__ ((packed)); unsigned long OemProductNamePtr __attribute__ ((packed)); unsigned long OemProductRevPtr __attribute__ ((packed)); unsigned char Reserved[222] __attribute__ ((packed)); unsigned char OemData[256] __attribute__ ((packed)); } VESA_INFO;
The __attribute__ modifiers are needed to make gcc pack the structure into the standard VESA layout, rather than adding pad bytes between some of the fields like it would normally do.
Having declared this structure, you can call VESA function 0x4F00 to fill it with information about the current driver. Because VESA was designed as a real mode API for use by 16 bit programs, this data must be transferred via a buffer in conventional memory with the dosmemput() and dosmemget() functions: see the DPMI chapter for details of this. The function below will copy the VESA driver information into a global VESA_INFO structure, returning zero on success or -1 if anything goes wrong (ie. no driver is available).
#include <dpmi.h> #include <go32.h> #include <sys/farptr.h> VESA_INFO vesa_info; int get_vesa_info() { __dpmi_regs r; long dosbuf; int c; /* use the conventional memory transfer buffer */ dosbuf = __tb & 0xFFFFF; /* initialize the buffer to zero */ for (c=0; c<sizeof(VESA_INFO); c++) _farpokeb(_dos_ds, dosbuf+c, 0); dosmemput("VBE2", 4, dosbuf); /* call the VESA function */ r.x.ax = 0x4F00; r.x.di = dosbuf & 0xF; r.x.es = (dosbuf>>4) & 0xFFFF; __dpmi_int(0x10, &r); /* quit if there was an error */ if (r.h.ah) return -1; /* copy the resulting data into our structure */ dosmemget(dosbuf, sizeof(VESA_INFO), &vesa_info); /* check that we got the right magic marker value */ if (strncmp(vesa_info.VESASignature, "VESA", 4) != 0) return -1; /* it worked! */ return 0; }
After calling the get_vesa_info() function you may want to examine a few of the values that it leaves in the vesa_info structure, notably the VESAVersion, Capabilities, and TotalMemory fields (see the VESA spec for details of these). Assuming that this call has succeeded, the next step is to work out which mode you want to use, and retrieve another information structure that is specific to this mode. VESA can in theory support an infinite variety of possible resolutions, but obviously most hardware can only handle a few specific modes. By far the most common is the 640x480 resolution, but most cards can also handle sizes of 640x400, 800x600, and 1024x768, and many can go up as high as 1280x1024 and 1600x1200 as well as occasionally supporting low resolution modes like 320x240 and 360x400, and oddball sizes like 512x512. There are a few standard mode numbers like 0x101 for the 640x480 256 color mode, and you will see a lot of tutorials and code that use these fixed values, but it is not a good idea to rely on them because the most recent version of the VESA specification warns that they may change in the future. This isn't a problem, though, because there is a perfectly good way to check what modes are available at runtime, which also has the advantage of letting your program support any strange modes that the driver might provide on future or different hardware, even if you didn't know about them when you wrote it.
Information about a particular mode can be obtained in a similar way to the main VESA information block, but using function 0x4F01 with a different structure, eg:
typedef struct MODE_INFO { unsigned short ModeAttributes __attribute__ ((packed)); unsigned char WinAAttributes __attribute__ ((packed)); unsigned char WinBAttributes __attribute__ ((packed)); unsigned short WinGranularity __attribute__ ((packed)); unsigned short WinSize __attribute__ ((packed)); unsigned short WinASegment __attribute__ ((packed)); unsigned short WinBSegment __attribute__ ((packed)); unsigned long WinFuncPtr __attribute__ ((packed)); unsigned short BytesPerScanLine __attribute__ ((packed)); unsigned short XResolution __attribute__ ((packed)); unsigned short YResolution __attribute__ ((packed)); unsigned char XCharSize __attribute__ ((packed)); unsigned char YCharSize __attribute__ ((packed)); unsigned char NumberOfPlanes __attribute__ ((packed)); unsigned char BitsPerPixel __attribute__ ((packed)); unsigned char NumberOfBanks __attribute__ ((packed)); unsigned char MemoryModel __attribute__ ((packed)); unsigned char BankSize __attribute__ ((packed)); unsigned char NumberOfImagePages __attribute__ ((packed)); unsigned char Reserved_page __attribute__ ((packed)); unsigned char RedMaskSize __attribute__ ((packed)); unsigned char RedMaskPos __attribute__ ((packed)); unsigned char GreenMaskSize __attribute__ ((packed)); unsigned char GreenMaskPos __attribute__ ((packed)); unsigned char BlueMaskSize __attribute__ ((packed)); unsigned char BlueMaskPos __attribute__ ((packed)); unsigned char ReservedMaskSize __attribute__ ((packed)); unsigned char ReservedMaskPos __attribute__ ((packed)); unsigned char DirectColorModeInfo __attribute__ ((packed)); unsigned long PhysBasePtr __attribute__ ((packed)); unsigned long OffScreenMemOffset __attribute__ ((packed)); unsigned short OffScreenMemSize __attribute__ ((packed)); unsigned char Reserved[206] __attribute__ ((packed)); } MODE_INFO; MODE_INFO mode_info; int get_mode_info(int mode) { __dpmi_regs r; long dosbuf; int c; /* use the conventional memory transfer buffer */ dosbuf = __tb & 0xFFFFF; /* initialize the buffer to zero */ for (c=0; c<sizeof(MODE_INFO); c++) _farpokeb(_dos_ds, dosbuf+c, 0); /* call the VESA function */ r.x.ax = 0x4F01; r.x.di = dosbuf & 0xF; r.x.es = (dosbuf>>4) & 0xFFFF; r.x.cx = mode; __dpmi_int(0x10, &r); /* quit if there was an error */ if (r.h.ah) return -1; /* copy the resulting data into our structure */ dosmemget(dosbuf, sizeof(MODE_INFO), &mode_info); /* it worked! */ return 0; }
This function is obviously only useful if you already know what mode number you should pass as the parameter, but that information can easily be obtained from the main VESA information block. This contains a list of all the possible modes that are supported by the driver, so you can write a little routine that will loop through all these modes, retrieving information about each one in turn until it finds the one that you are looking for. For example:
int find_vesa_mode(int w, int h) { int mode_list[256]; int number_of_modes; long mode_ptr; int c; /* check that the VESA driver exists, and get information about it */ if (get_vesa_info() != 0) return 0; /* convert the mode list pointer from seg:offset to a linear address */ mode_ptr = ((vesa_info.VideoModePtr & 0xFFFF0000) >> 12) + (vesa_info.VideoModePtr & 0xFFFF); number_of_modes = 0; /* read the list of available modes */ while (_farpeekw(_dos_ds, mode_ptr) != 0xFFFF) { mode_list[number_of_modes] = _farpeekw(_dos_ds, mode_ptr); number_of_modes++; mode_ptr += 2; } /* scan through the list of modes looking for the one that we want */ for (c=0; c<number_of_modes; c++) { /* get information about this mode */ if (get_mode_info(mode_list[c]) != 0) continue; /* check the flags field to make sure this is a color graphics mode, * and that it is supported by the current hardware */ if ((mode_info.ModeAttributes & 0x19) != 0x19) continue; /* check that this mode is the right size */ if ((mode_info.XResolution != w) || (mode_info.YResolution != h)) continue; /* check that there is only one color plane */ if (mode_info.NumberOfPlanes != 1) continue; /* check that it is a packed-pixel mode (other values are used for * different memory layouts, eg. 6 for a truecolor resolution) */ if (mode_info.MemoryModel != 4) continue; /* check that this is an 8-bit (256 color) mode */ if (mode_info.BitsPerPixel != 8) continue; /* if it passed all those checks, this must be the mode we want! */ return mode_list[c]; } /* oh dear, there was no mode matching the one we wanted! */ return 0; }
And at last, you are ready to actually select a VESA graphics mode and start drawing things onto the screen! This is done by calling function 0x4F02 with a mode number in the BX register, eg:
int set_vesa_mode(int w, int h) { __dpmi_regs r; int mode_number; /* find the number for this mode */ mode_number = find_vesa_mode(w, h); if (!mode_number) return -1; /* call the VESA mode set function */ r.x.ax = 0x4F02; r.x.bx = mode_number; __dpmi_int(0x10, &r); if (r.h.ah) return -1; /* it worked! */ return 0; }
The SVGA video memory is located at physical address 0xA0000, the same as in mode 13h, but there is one small problem with this: there simply isn't enough room for it all to fit there! The original DOS memory map only included space for 64k of video memory between 0xA0000 and 0xB0000, which is fine for a 320x200 resolution but nowhere near enough for a 640x480 screen (that takes up 300k of framebuffer space, and higher resolutions need even more). The SVGA hardware designers solved this problem by using a banked memory architecture, where the 64k VGA memory region is treated as a sliding window onto the larger expanse of real video memory inside your card. To access an arbitrary location on the SVGA screen you must first call VESA function 0x4F05 to tell it which bank you want to use, and then write to a memory location within that bank. You can set the bank with the function:
void set_vesa_bank(int bank_number) { __dpmi_regs r; r.x.ax = 0x4F05; r.x.bx = 0; r.x.dx = bank_number; __dpmi_int(0x10, &r); }
Using this, a simple putpixel function can be implemented as:
void putpixel_vesa_640x480(int x, int y, int color) { int address = y*640+x; int bank_size = mode_info.WinGranularity*1024; int bank_number = address/bank_size; int bank_offset = address%bank_size; set_vesa_bank(bank_number); _farpokeb(_dos_ds, 0xA0000+bank_offset, color); }
Note: Many VESA tutorials, and indeed a few actual production programs, assume that the SVGA memory banks will always be exactly 64k in size. This is true on about 95% of cards, but there is some hardware with bank sizes of 4k or 16k, so the correct approach is to read the bank size from the WinGranularity field of the mode information structure as demonstrated above.
Note 2: The bank switch function is very slow! This simplistic putpixel routine is far too inefficient to be useful in real life. It can be improved by making the set_vesa_bank() function only bother to change banks if the new value is different to the previous one, and you should try to optimize your more complex drawing functions to use the minimum possible number of bank switches.
Because bank switching is so slow and awkward, it is often better to do all your drawing onto a framebuffer array in regular memory, and copy it across to the VESA screen in a single pass once the image is complete. This can be done with a function along the lines of:
void copy_to_vesa_screen(char *memory_buffer, int screen_size) { int bank_size = mode_info.WinSize*1024; int bank_granularity = mode_info.WinGranularity*1024; int bank_number = 0; int todo = screen_size; int copy_size; while (todo > 0) { /* select the appropriate bank */ set_vesa_bank(bank_number); /* how much can we copy in one go? */ if (todo > bank_size) copy_size = bank_size; else copy_size = todo; /* copy a bank of data to the screen */ dosmemput(memory_buffer, copy_size, 0xA0000); /* move on to the next bank of data */ todo -= copy_size; memory_buffer += copy_size; bank_number += bank_size/bank_granularity; } }
The description of the bank switching mechanism given above is actually a slight simplification of the true state of affairs, because VESA supports two different banks (described as "windows" in the spec), which can be configured in a variety of ways depending on the hardware. Usually there will only be one bank which is used both for reading and writing the video memory, but on some cards you may have two windows using different address ranges (eg. one from 0xA0000 to 0xB0000 and another from 0xB0000 to 0xC0000, or two 32k chunks from 0xA0000 to 0xA8000 and 0xA8000 to 0xB0000), or you might have one window for writing to the screen and a different one for reading from it, which can be positioned independently of each other. You don't need to worry about this when drawing onto the screen, as long as you are careful to always use the WinSize and WinGranularity values rather than assuming the banks will always be 64k in size, but it is essential to check the window setup before you try to read pixel values back from the display. If the bottom two bits in the mode_info.WinAAttributes field are set, the first window is readable and you can proceed as normal. Otherwise you will have to use the second window for your read operations, which means changing the set_vesa_bank() function to put a 1 in the BX register, and writing to mode_info.WinBSegment*16 rather than the default 0xA0000 address.
References (broken links, sorry, use your favorite search engine)