We briefly went over the pixel arrangement in VGA's mode 19 (for simplicity, we will refer to it from now on as mode 13h). Now, let's look at how we actually write code to make this work.
As we already discovered, to find the exact memory location for any pixel coordinate we just simply use this formula:
F(X, Y) = 655360 + 320Y + X
To set a pixel to any color we want, just move its numeric color code to the memory address derived from this function. So let's examine how to put this into code so that a computer can do it.
Well, one unfortunate fact of life here is that programs written for the DOS platform do not make this very easy. This is because of the segmented architecture of the early Intel 8088 chip and DOS's incredibly slow movement away from this type of architecture. As a result, programs written for DOS had to use this architecture, even though the underlying processor could run using flat memory architecture. This is what is known as the "real mode." (There is a "protected mode" that will allow you to use the natural memory address, but that's beyond the scope of what we will discuss today.) To understand this mode, we must understand how DOS (or actually the 8088) addresses memory.
The 8088 microprocessor can address 1 megabyte of memory. It does this with index and pointer register that can each address 64K of memory. A segment of memory consists of 64K of memory so naturally the CPU has segment registers in order to address more than 64K. The segment registers can address 65536 segments with each segment being 16 bytes apart. Since each segment consists of 64K bytes but are only 16 bytes apart there is naturally a lot of overlapping of the segments themselves. Memory addresses found in DOS programs usually look similar to this:
B800:051C
The first four digits before the colon (:) represent the hexadecimal value of the segment. The last four digits represent the hexadecimal value of the "offset." An offset is just simply an address within the segment. To get the natural address, we can just simply multiply the value of the segment by 16 (because each segment is 16 bytes apart) and add the offset. Like so:
segment = B80016 or 47104 offset = 051C16 or 1303 segment * 16 = B800016 or 753664 segment * 16 + offset = B851C16 or 754972 natural address = B851C16 or 754972 natural address = segment * 16 + offset
Well now that we know how to make a natural address out of a segment:offset address, now we need to do just the opposite in order to make DOS address pixels for our programs. To do that, we just simply take the natural address, divide it by 16 to get the segment and use the remainder for the offset.
X = 5 Y = 10 F(X, Y) = 655360 + 320Y + X F(X, Y) = 658565 This is the natural address, to get the segment:offset address we must do this: 658565 / 16 = 41160 r 5 segment = 41160 or A0C816 offset = 5 or 516 segment:offset = A0C8:0005* *Extra zeros were added to make the offset 4 digits long.
Using this method we could always get the proper address into the 8088. There is a better way though. We just learned that while each segment is only 16 bytes apart, a single segment can actually address 64K (65536) bytes. Considering that mode 13h has 320x200 pixels and that each pixel is 1 byte long, mode 13h consumes 64000 bytes (320 x 200 x 1 byte). These 64000 bytes can comfortably fit in a single segment and that segment begins at memory location 655360. Observe:
segment = natural address / 16 offset = natural address (modulus*) 16 segment = 655360 / 16 segment = 40960 or A00016 offset = 655360 (modulus) 16 offset = 0 or 016
In this case, the segment for our video memory is A00016 and the offset will be derived from the formula, F(X, Y) = 320Y + X. So each pixel in video memory is addressed by the 8088 processor like so:
A000:(320Y + X)
This is no accident, this was the way the memory architecture of the early IBM PC's and compatibles were set up. In fact, this memory architecture remains in place even to this day. Anyone whoever used DOS programs before Windows 95 came out, and even after, knows something about the 640K memory barrier where 640K of "conventional" memory is all that is available to a typical DOS program. The video memory lies just beyond this 640K of conventional memory on a PC memory map.
Since none of us will be coding eveything in assembly language (at least, I'm assuming that most of us won't) we can go ahead and look at C code that will write pixels to the video screen. I have seen many ways that this is done but here are a couple of my favorites.
The first function is an example derived from a similar function written by Dave Roberts in his book "PC Game Programming Explorer." I've modified it a bit for simplicity but it basically works the same way.
void SetPixel(int X, int Y, unsigned char C) { unsigned char far *videoMem; /* Set our far pointer * to the address of the pixel * with 0xA000 as the segment * and 320Y + X as the offset. */ videoMem = (unsigned char far *)MK_FP(0xA000, 320 * Y + X); /* Write the pixel color to memory */ *videoMem = C; }
At first glance, this function looks kind of complicated. But keep in mind that all we did was get the address of the pixel we wanted to write the color to and write the color.
First we create a far pointer and call it 'videoMem.' A far pointer is simply a variable that stores a segment and an offset to address a full megabyte of memory. The MK_FP function takes the segment that we figured out earlier to be A00016 and the offset that is derived from the formula (320Y + X). The value of C (the color code) is then moved to the address pointed to by the far pointer.
Another code example that I like better involves making a far pointer, that we call videoMem, a public (or global) variable. This makes sense since the pixels in the video memory will be addressed in a lot more places than in just one part of the program. A single public variable allows for a single point of reference. To do this, we declare the videoMem far pointer public and assign the segment / offset value of the beginning of the video RAM to it (A000:0000).
unsigned char far *videoMem = (unsigned char far *)0xA0000000L;
Using the pre-initialized far pointer, we can easily index it to the address of whatever pixel we choose.
void SetPixel(int X, int Y, unsigned char C) { videoMem[Y * 320 + X] = C; }
That's it! Just one little line compared to the first example that required 3 lines. One to initialize a pointer, one to assign the address, and one to write the pixel. With this example, the pointer is already initialized and assigned a base segment address. All that is required is to determine the offset (via an index to the pointer) and write the pixel. Simpler and faster because 'videoMem' is declared globally.