Copy Link
Add to Bookmark
Report

DnA 9-12: VGA Programming

eZine's profile picture
Published in 
DnA Electronic Magazine
 · 16 May 2023

VGA Programming

Although this subject is not one that normally accompanies a typical P/H magazine, many people have asked questions about both VGA and SVGA graphics programming, and I've decided to write up the article as a response to all of them, instead of replying to each individually. In this article, I'm not going to address SVGA programming, since it's such a huge subject, and one that I need to work on more. While this is far from a step by step tutorial in how to infiltrate a specific system, I like to think that broad knowledge of all aspects of programming are an integral part of being a successful hacker. In that light, I hope you read and enjoy what I have to present here. I have drawn this information from a tremendous variety of sources. Many are books, and many are people whose names are forgotten, but whose contributions are not.

What's Required of You

I assume you have some programming knowledge. If you don't, this article will be virtually useless to you. All my sample code is in either C or assembler. Most of it is pretty simple stuff, so if you know either one even to some small amount, you'll be able to handle this pretty easily. This is geared towards those who understand programming well, and want to learn more about how to take full control over their systems. Understanding video cards completely is a good start, and something that will take quite a bit of time to get down. If you aren't a programmer, don't bother reading on, you're just going to get bored to death learning about VGA internals and the like. If you are, read on! You'll like what you can do once you're done.

VGA architecture

Before I can go into detail on how to program a VGA card to do what you want, I need to describe some of it's architecture. First of all, standard VGA graphic's highest useful mode is 320 x 200 in 256 colors. While it is a bit crude looking, it is very useful for plenty of applications. If you want to do better, you will need to start dealing with SVGA graphics, which are far less standard than VGA. I will go into SVGA graphics at another time. The subject is just too damn big to put into this introductory text. Next, video displays are what are called memory mapped devices. What does this mean? Well, everyone knows that there is memory on your VGA card. Typically, your standard SVGA card has 1 megabyte of VRAM on the board. How do you access this memory on the card? The answer is that an entire segment of memory(64k bytes) is reserved in your first megabyte of system memory. Anything you write to this segment of memory gets copied to the VGA card's memory directly. The base memory is 'mapped' into the VRAM on the VGA card. Whatever is put into the VRAM is what is displayed on the screen. It's simply a matter of writing the right information in the right order.

Accessing the Memory Map

In standard VGA modes, you can't access more than the first 64k of RAM(out of the typical one meg) on the card. This is just a limitation put in place by the current standards. DOS actually sets aside two whole segments of memory for video display. However, one is dedicated to text displays, and the other is for true graphics. The size of the memory map for 320 by 200 is exactly 64000(320 * 200) bytes. The segment for graphics displays is located at 0A000h. Writing to this segment will produce changes on your screen assuming you are currently in graphics mode.

Understanding the Memory Map

Drawing single pixels on the screen is an extremely simple task. Doing so requires you to write a single byte into the graphic memory map at 0A000h. However, it's probably important to you *where* on the screen that pixel is going to show up, so it's needs to be discussed how the memory map relates to the pixels on the screen. To do this, we need to consider the coordinate system for IBM monitors. First of all, the starting corner of the screen is the upper left. Think of that corner of the screen as (0,0). X increases as you move further to the right. Y increases as you move down. Obviously, in this graphics mode, you can have X values from 0 to 319, for a total of 320 pixels across the screen. Similarly, you can have pixels from 0 to 199 for Y, totalling 200 pixels vertically. As I've mentioned, the full address of the beginning of the VGA screen is at 0xA000:0x0000. The '0x' before the numbers denote the fact that they are in hexadecimal notation(base 16). To move on to the next pixel (1,0), you would read from the offset 0x0001. Pixel (2,0) would be 0x0002, and so on. This is very easy, until you hit the end of the line. The last pixel at the top of the screen (319,0) is at offset 0x013F(319 decimal). The next line starts at the very next byte in memory. So, the offset for (1,0) is 0x140(320 decimal). (1,1) is 0x0141. (1,2) is 0x0142 and so on. Understanding how the memory is mapped on the screen is important, since it's this map that you use to make useful images. If you don't understand this, you've got to go back and read it again until you do.

Imagine the asterisks below are the pixels of the screen, starting at the upper left. The numbers to the left and right of the 'pixels' are the offsets of the first and last pixels on that line, respectively.

                           VGA SCREEN: 
0 |****************************************************| 319
320 |****************************************************| 639
640 |****************************************************| 959
960 |****************************************************|1279
1280|****************************************************|1599

Formula for Pixels

Now that you understand the memory mapping, how do you find the offset for a given pixel on the screen? This is the toughest part of writing to VGA screens. The algorithm itself is extremely easy. The offset of an arbitrary pixel found with this algorithm: Offset = (Y * 320) + X

This formula makes sense if you think about how the screen is laid out. If you want to move down to line number Y, you have to add 320 to the offset to reach that point for every line you move down. If you don't understand what I mean, just accept that the formula is correct, and figure it out for yourself later. Now that you know the formula, you can write a generic function or macro that does this in either C, assembly, or some other language. Personally, considering how simple an algorithm this is, I'd go ahead and code it in assembly. Besides, it's handy to stay away from the multiplication functions that high level languages use. Here is some very quick code to determine offsets given an X and a Y value:

MOV         AX,Y 
MOV BX,X
MOV CL,6
SHL AX,CL ; Fast multiply of Y times 64
ADD BX,AX ; Add Y to X
ADD AX,AX ; Multiply Y times 2 again
ADD AX,AX ; Multiply Y times 2 again
ADD BX,AX ; Add total result into BX

If you have no way of using assembly, by all means use similar code in whatever language you wish. For example, a function in C would look like this:

unsigned char far *screen; 
screen = MK_FP(0xA000,(y * 320) + x);

Here's a tiny C program to calculate offsets for you, in both Decimal and Hex:

#include <stdio.h> 
#include <dos.h>

void main(void)
{
int x,y;
unsigned int offset;
printf("Input X and Y coordinates for VGA 13h mode offset:\n");
scanf("%d",&x);
scanf("%d",&y);
offset = (y * 320) + x;
printf("%d, 0x%x\n",offset,offset);
}

Most of my example code in this article will be in C, since with this graphic mode, it's not very critical that it be optimized to death. When dealing with SVGA graphics, writing efficient code is nearly impossible in C. But we're not there yet, and we should be thankful for that.

Accessing Memory

Now that we have our memory address stored into a far pointer, how do we use that information? In fact, it's extremely easy. You will only need to deal with offsets once the '*screen' pointer has been setup. You will never need to change the value of *screen, since you can now access screen as an array of characters. To write a pixel of arbitrary value '87' to the location (0,0) on the monitor:

unsigned char far *screen; 
screen = MK_FP(0xA000,0);
screen[0] = 87;

That's all there is to it! Once you're in graphics mode, you simply choose the offset of the pixel you want to write, and put that offset into the screen array to be dereferenced. We can use the above formula we learned to make a general function to write a pixel to any spot on the screen:

int plotpoint(int x, int y, char color) 
{
if( (x > 319) || (x < 0) || (y > 199) || (y < 0) )
return 0;
screen[(y * 320) + x] = color;
return 1;
}

This code does assume that the '*screen' pointer is a global variable. I'm sure we all know that using global variables is 'bad' by most standards. Well, damn it, the screen object is something that lots of functions need access to, and there's only one screen anyway, right? Sending another 32 bit pointer on the stack every time you want to use a subroutine that accesses the screen is wasting time and space. This routine returns a 1 if it's successful, and 0 if the X or Y coordinates were out of bounds.

Reading a pixel is just as easy as setting it. You use the same global *screen pointer to access it, but read from the memory location rather than writing to it. Here it is:

int readpoint(int x, int y, char *color) 
{
if( (x > 319) || (x < 0) || (y > 199) || (y < 0) )
return 0;
color = screen[(y * 320) + x];
return 1;
}

This subroutine returns the same values as the one above, and it also returns the value of the color. Remember you have to send the function the pointer to your color variable when calling this routine, thusly:

unsigned char pixel; 
readpoint (45,56,&pixel);

All this is pretty straight forward coding. C is very flexible when it comes to arranging memory. The most difficult part is managing all the memory tasks by yourself. Don't be surprised when you get lots of null pointer assignments and the like. Working with memory and pointers is extremely taxing and tedious. Getting it right is tough, but rewarding in the end.

As it turns out, dereferencing a far pointer every time you want to write or write a pixel on the screen isn't terribly efficient. If you are going to read or write in a row, it is a much better idea to optimize that memory access at the same time. You can do it with C, or write yourself a very simple, and highly optimized assembly routine to do the same thing. Let's say you have a buffer(array of chars) which you want to write directly to the screen. How can you move that to the position you want on the screen directly without dereferencing far pointers a lot? Here it is in assembly:

g_memwrite proc uses ES SI DI, inbuf:WORD, start:WORD, length:WORD 
MOV CX,length ; How many bytes to write
MOV AX,0A000h ; Video Memory segment
MOV SI,inbuf ; offset of byte array to be written
MOV ES,AX ; Set ES to video memory segment
MOV DI,start ; Where on screen to start(offset)
REP MOVSB ; Write CX bytes to screen
RET
g_memwrite endp

Here are some useful functions in assembly for working with mode 13h VGA. For setting the VGA screen to graphics mode:

void set_vgamode(void); 

set_vgamode PROC
MOV AX,13h
INT 10h
RET
set_vgamode ENDP

For setting back to 80x25 text mode:

void set_textmode(void); 

set_textmode PROC
MOV AX,3
INT 10h
RET
set_textmode ENDP

For clearing the whole screen to a given color:

void clearscrn(char color); 
clearscrn PROC USES ES DI, color:byte
MOV AX,0A000h
MOV ES,AX ;Point ES:DI to
XOR DI,DI ; the screen map
MOV CX,64000 ;Set repeat to 64000
MOV AL, color ;Color to be written to all pixels
REP STOSB ;Write all pixels at once
RET
clearscrn ENDP

For writing a pixel to the screen at (X,Y) with a given color. Requires a 286 to execute this code, since it uses immediate for the shifts:

void plotpoint(int x, int y, char color); 

plotpoint proc USES ES, X:word, Y:word, color:byte
MOV AX,0A000h
MOV ES,AX
MOV AX,Y
MOV BX,X
SHL AX,6
ADD BX,AX
SHL AX,2
ADD BX,AX
MOV AL, byte ptr color
MOV ES:[BX], AL
RET
plotpoint ENDP

Now that we've gotten into how to write directly to the video screen, let's look at another important subject in video programming. The VGA palette may be the most difficult aspect of VGA programming to grasp.

VGA Palette

Now that we understand how to write certain values into video memory, it's time to discuss how those values are translated into color. A palette is how you describe what colors are going to be shown on the screen for a given value. Let's say you write the value '68' to some pixel on your screen. How does your VGA card decide what color is associated with that number 68? How do you set your card to display a given color? The VGA palette for each of the 256 colors is a 3 byte series. The first byte is the value for Red, the second for Green, and the third for Blue. Each of those bytes can only use the bottom 6 bits, meaning that their values can be between 0 and 63 only. Being at a value of 0 means that the color is completely turned off. The color black is achieved by setting all three bytes to 0. Setting a color to 63 turns it full on. Setting all three bytes to 63 will be the color white. Everything else in between can be achieved with different combinations. There are a total of 262,144 colors that can be displayed on a VGA card, 256 of which can be displayed at once. Since there are 256 colors in the VGA palette, the palette in memory takes up 768 bytes (256 * 3). If you are going to manipulate the palette, you're going to want to keep it in memory, since reading from the card is slow and difficult.

Manipulating the Palette

Writing the values you want to the VGA card is actually quite simple. While there are other considerations when you're doing this, all you have to do is write three bytes to the correct IO port. First, to write to the palette for color number 68 for example, you have to write the number 68 to port 0x3C8. After that, you have to write the three byte values in order to port number 0x3C9. If you don't understand this, look at the pseudocode example:

write 68 to port 0x3C8 
write Red byte to port 0x3C9
write Green byte to port 0x3C9
write Blue byte to port 0x3C9

It's quite simple, as you can see. The only difficult part of this is setting the values of the palette to values you want. There is one level of complication that needs to be added to this simple model though. On some systems, this will cause lots of static, or snow on the screen whenever a palette register is set. This is caused for some esoteric reason on crappy VGA cards. The way around this is to only write to the screen while the monitor is retracing. If you don't know how a monitor paints the screen, it moves an electron beam along each of the pixel lines, and when it gets to the bottom, it has to move the beam back up to the upper left of the screen to start drawing again. While it's moving back up to the top of the screen, obviously nothing is being written to the monitor. This is good for us, since no static shows up at this time either. All we have to do is make sure we're in a retrace state, and then set our palette registers. Checking retrace requires that you read port 0x3DA. Reading a port means that you get a byte of data in return. Only the third bit of data is useful to us right now. If that bit is a one, then the screen is in a retrace currently, and anything written to it will have little or no snow. We need to make sure that we don't accidentally come by the retrace at the very last second, and write a whole bunch of data to the VGA card even after it's done with the retrace. To stop that, we will have two loops that wait for the retrace. That being the case, lets examine the new p-code.

non_retrace: 
read port 0x3DA
if bit 3 = 1 goto non_retrace

retrace:
read port 0x3DA
if bit 3 = 0 goto retrace

write 68 to port 0x3C8
write Red byte to port 0x3C9
write Green byte to port 0x3C9
write Blue byte to port 0x3C9

Typically, about 128 of the 256 color palette can be written in a single retrace, so loop the last statement for 128 times:

non_retrace: 
read port 0x3DA
if bit 3 = 1 goto non_retrace

retrace:
read port 0x3DA
if bit 3 = 0 goto retrace

for x=0 to 127
{
write x to port 0x3C8
write Red byte for color x to port 0x3C9
write Green byte for color x to port 0x3C9
write Blue byte for color x to port 0x3C9
}

Rewriting this in another language shouldn't take too long. Here is the code to do just this in C:

set_vgapalette(char *p)  //p is the 768 byte palette array 
{
unsigned int i,j,end;
for(i=1; i<=2; i++) //Do twice
{
end = 128 * i;
while( 1 == inp(0x3DA) & 0x08 ); //Loop while bit 3 = 1(retrace)
while( 0 == inp(0x3DA) & 0x08 ); //Loop while bit 3 = 0(not)
for(j=128 * (i - 1); j<end; j++) //Write first or second 128
{ // colors to palette registers
outp(0x3C8,j); //Color to be written next
outp(0x3C9,p++); //Red byte of palette color
outp(0x3C9,p++); //Green byte..
outp(0x3C9,p++); //Blue byte..
}
}
}

This is a pretty quick little routine for setting the palette. Of course, this sets the entire palette every time you call it. If you only are changing a small part of the palette, it'll be faster to write a function that sets only that portion you changed. Another option is to rewrite this in optimized assembly code. Personally, I think all your VGA primitives should be written in assembly for the sake of speed and size. If you don't know assembly though, you're pretty much out of luck. All I can do is give you some standard routines to use for yourself, and recommend you try learning it. It's a big help to you when writing in every language. For instance, many C or Pascal compilers have an option of compiling your C code to assembly. Being able to see what the compiler does with your code can help you optimize even straight C routines a lot. For instance, in the code above, I put made the variable 'end' because I looked at how C assembles the FOR loop, and realized that it calculates the second part of the FOR statement every time it loops. Instead of multiplying I*128 every time the loop comes by, it just compares it with the memory location 'end'. It's much faster, and makes the loop quicker, so there's no snow on the screen. Enough crap on the wrong subject.

Let's get back to palette functions. Since you can change the palette while you've already got something displayed on the screen, interesting effects can be produced. Let's say you want to fade your screen at the end of a sequence to black. The value black is achieved by setting all three palette colors to zero. So, if we want to fade the entire palette to black over time, we just have to subtract one from each of the 768 byte colors from the palette in memory. Look at the code, not at my writing:

for(i=0; i<768; i++) 
{
if palette[i] > 0 then palette[i] = palette[i] - 1;
}
set_vgapalette(palette);

This code does just what I mentioned. It subtracts one from each byte color, and once it's done all 768 of them, it displays the new palette using the set_vgapalette function. Many other interesting functions can be achieved using palette changing routines such as this. You could fade to white, slowly swap from one palette to another, rotate palettes to create a moving effect. For instance, you could make an fiery explosion seem to move by changing the palette from white to yellow to red slowly. The particular application is up to you; use your imagination.

That's about all there is too it. There are lots of other graphics modes you could use, and lots of other things that could be mentioned. However, only so much can be taught to you. Much of the understanding necessary for writing good graphics code must simply be learned by trial and error coming from hands on programming. While I don't fool myself into believing many people will benefit from this introduction, I hope someone enjoyed it. I have spent many, many hours learning VGA and SVGA programming, and I hope this little beginning will be enough for at least a few individuals to start off on their own. If you read this and have questions, or would like to see another article written on a more specific subject, please do mail me on Digital Decay, or some board that picks up DNAnet. I'll probably do an article on SVGA cards at a later time, but I need more time to work on that. Until such time, pleasant programming and happy hacking.

Zephyr [Cosys - Digital Decay] 8/94

← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT