The VGA and 256 Colors
Copyright Dr. William T. Verts April 28, 1996
This document contains some notes for those of you who use the
VGA on a PC compatible. Most of the time, in Turbo Pascal or
Turbo C, the Borland .BGI is sufficient for doing graphics, but
when you need 256 colors there isn't really a "standard" .BGI
file for 320x200 mode. There are a few versions out on the net,
but it turns out that if you are doing simple graphics that
doesn't have to be compatible across all graphics adapters, then
it is really pretty easy to write your own code for this one
graphics mode.
In 320x200 256-color mode, the screen is an array of 64000
contiguous bytes, arranged as 200 rows of 320 bytes,
with one byte per pixel, located in memory at absolute
address $A000:$0000. So, if you can convince the adapter
to go into this mode, plotting stuff on the screen is as
easy as poking bytes into memory at the appropriate offset
from $A000:$0000.
If we can get into graphics mode, we must also be able
to get out of graphics mode and go back to text mode.
The other issue to deal with is that of the palette, or
color look-up table. This is a list of 256 byte-triples,
stored in the VGA card. We must be able to get the
current palette from the VGA card, and also tell the VGA
card to use a palette that we have created.
These five problems outlined above require some detailed
knowledge of the VGA adapter in this mode. These cases
require that we either use some assembly language, or
some non-standard procedure calls, or both. In summary,
these problems are:
- Getting into 320x200 256-color graphics mode.
- Getting out of graphics mode, back to text mode.
- Fetching the current palette from the VGA adapter.
- Updating the palette in the VGA adapter with a new one.
- Knowing the layout of the graphics screen in memory.
Getting into Graphics Mode
There is no specific call to get into this graphics mode, but
we can call the assembly language code to switch the adapter.
There are two main methods. In direct 8088 assembly language
(or using the ASM directive in Turbo Pascal), you use the
following instructions:
MOV AX, 13H
INT 10H
The other way is to use the support routines for assembly language
provided in Turbo Pascal, which effectively does the same thing.
The "Registers" type is declared in the DOS unit. "FillChar"
can fill a block of memory with the same value quickly, as
long as you know the address of the block, the number of bytes
to change, and the value to change them to. The code is as
follows:
Procedure InitGraph ;
Var R : Registers ;
Begin
FillChar (R, SizeOf(R), 0) ;
R.AX := $0013 ;
Intr ($10, R) ;
End ;
Getting Out of Graphics Mode
Getting back to text mode is as easy as getting into graphics
mode. The first way is to use the assembly language:
MOV AX, 03H
INT 10H
You can also use the "Registers" type, and do the same thing
from Pascal, as in the following code:
Procedure CloseGraph ;
Var R : Registers ;
Begin
FillChar (R, SizeOf(R), 0) ;
R.AX := $0003 ;
Intr ($10, R) ;
End ;
The VGA Palette Type
Working with the palettes is a little bit trickier. First off,
you need to have a type declared that is assignment compatible
with the palette registers in the VGA card. This can be done
with the Color_Table type as follows (the record type
Color_Type is the same as in earlier discussions):
Color_Type = Record
Red : Byte ;
Green : Byte ;
Blue : Byte ;
End ;
Color_Table = Array [Byte] Of Color_Type ;
Note that this is not exactly the same as the Palette
type discussed in Dr. Bill's Notes on
Palettes, but is structurally compatible with the array
component of the Palette type. If you are clever,
you can use the Color_Table definition in the
Palette type.
There is another "gotcha" as well when dealing with the VGA.
Even though the Color_Type record contains three bytes,
the VGA only uses the lower six bits of each byte for the color
value. This gives 64 variations per color instead of 256
(64x64x64=262144 possible colors), and means that the values
should be shifted left by 2 bits (multiplied by 4) after
they are extracted from the VGA to get approximately correct
byte values for true RGB color, and shifted right by 2 bits
(divided by 4) before they are placed into the VGA.
Fetching the Current VGA Palette
This is a little bit more complicated than simply going into
or out of graphics mode, as there are a lot more registers
to establish. Certainly this can be done very simply in
assembly language, but it is as easy in Pascal, as follows:
Procedure Get_Palette (Var P:Color_Table) ;
Var R : Registers ;
I : Byte ;
Begin
FillChar (R, SizeOf(R), Zero) ; (* Initialize Record *)
With R Do
Begin
AX := $1017 ; (* Command to get Palette *)
BX := 0 ; (* Starting Palette Entry *)
CX := 256 ; (* Number of Entries *)
ES := Seg(P) ; (* Address of Palette *)
DX := Ofs(P) ; (* Address of Palette *)
End ;
Intr ($10, R) ; (* Do the command *)
(*--------------------------------------------------------*)
(* Turn 6 bit VGA palette entries into 8 bit RGB bytes *)
For I := 0 To 255 Do
With P[I] Do
Begin
Red := Red SHL 2 ;
Green := Green SHL 2 ;
Blue := Blue SHL 2 ;
End ;
End ;
Creating a New VGA Palette
Going the other direction and placing a new palette into
the VGA is almost identical to extracting the current palette
from the VGA. The assembly language command is very similar,
and color byte values must be shifted right by 2 bits to convert
them into the 6 bit VGA values before storing them in the VGA.
Procedure Put_Palette (P:Color_Table) ;
Var R : Registers ;
I : Byte ;
Begin
(*--------------------------------------------------------*)
(* Turn 8 bit RGB values into 6 bit VGA palette entries *)
For I := 0 To 255 Do
With P[I] Do
Begin
Red := Red SHR 2 ;
Green := Green SHR 2 ;
Blue := Blue SHR 2 ;
End ;
(*--------------------------------------------------------*)
FillChar (R, SizeOf(R), Zero) ; (* Initialize Record *)
With R Do
Begin
AX := $1012 ; (* Command to put Palette *)
BX := 0 ; (* Starting Palette Entry *)
CX := 256 ; (* Number of Entries *)
ES := Seg(P) ; (* Address of Palette *)
DX := Ofs(P) ; (* Address of Palette *)
End ;
Intr ($10, R) ; (* Do the command *)
End ;
The Memory Layout
Luckily for us, the screen 320x200 mode is under the 65536 byte
limit of a single data structure on an Intel machine. This means
that we can easily treat the 64000 bytes of the screen as a single
array, without having to do any fancy paging techniques. Since we
know the size, layout, and base address in memory of the screen, our
additional constant, type, and variable declarations become fairly
simple:
Const
Zero = 0 ;
GetMaxX = 319 ;
GetMaxY = 199 ;
Type
VGA_Linear = Array [0..63999] Of Byte ;
VGA_Array = Array [Zero..GetMaxY, Zero..GetMaxX] Of Byte ;
Var
Screen_Linear : VGA_Linear Absolute $A000:$0000 ;
Screen_Array : VGA_Array Absolute $A000:$0000 ;
Note that we have two different arrangements of the arrays at
location $A000:$0000. The first one is a simple
single dimension array, and the second is a two dimensional
array. The first can be used to flood the entire screen
with a single value (as in clearing the screen), and the
second allows us to address individual pixels.
Saving and Restoring the Screen
With the types above, it becomes a trivial matter to capture
the entire screen, then restore it later. All we need is a
local array of the correct type (or a pointer to that type,
since it is easier to allocate the storage off of the heap
than to use all 64K of the free variable area for a single data
structure), then saving and restoring
the screen are each a variable-to-variable copy, as follows:
Type VGA_Pointer = ^VGA_Array ;
Var Save_Screen : VGA_Pointer ;
(*-----*)
New (Save_Screen) ; (* Allocate the storage *)
Save_Screen^ := Screen_Array ; (* Capture the screen *)
Screen_Array := Save_Screen^ ; (* Restore the screen *)
Dispose (Save_Screen) ; (* Deallocate the storage *)
Then, of course, a file of screens means that a simple form
of animation can be achieved by reading frames one at a time
from the file and blasting them straight into the video
buffer:
Type Screen_File = File Of VGA_Array ;
Var Infile : Screen_File ;
Temp : VGA_Pointer ;
(*-----*)
New (Temp) ;
Assign (Infile, 'INFILE.DAT') ; (* Use appropriate DOS filename *)
Reset (Infile) ;
While Not EOF(Infile) Do
Begin
Read (Infile, Temp^) ;
Screen_Array := Temp^ ;
End ;
Close (Infile) ;
Dispose (Temp) ;
PutPixel and GetPixel Routines
These are the two most critical routines for graphics, and they
are both effectively "clipping + array indexing". I think they
are pretty much self-explanatory (note that indexing the array
with locations X and Y requires that the Y value be the first
index):
Procedure PutPixel (X,Y:LongInt ; Color:Byte) ;
Begin
If (X >= Zero ) And
(X <= GetMaxX) And
(Y >= Zero ) And
(Y <= GetMaxY) Then Screen_Array [Y, X] := Color ;
End ;
Function GetPixel (X,Y:LongInt) : Byte ;
Begin
If (X < Zero) Or
(X > GetMaxX) Or
(Y < Zero) Or
(Y > GetMaxY) Then GetPixel := Zero
Else GetPixel := Screen_Array[Y, X] ;
End ;
Clearing the Screen
Now, we can use the VGA_Linear type and the
Screen_Linear definition to set the entire screen to
a single color. Certainly we can use a FOR loop, as in
the code:
FOR I := 0 To 63999 Do Screen_Linear[I] := Color ;
but we can use the extremely fast FillChar procedure of
Turbo Pascal to accomplish the same feat:
Procedure Fill_Screen (Color:Byte) ;
Begin
FillChar (Screen_Linear, SizeOf(Screen_Linear), Color) ;
End ;
Procedure ClearDevice ;
Begin
Fill_Screen (Zero) ;
End ;
Horizontal and Vertical Lines
Because we know the layout of the screen in memory, these
specialized line types can be made to run very fast (again
leveraging the FillChar procedure in the horizontal line
routine).
The slow way of performing these routines is to
use a FOR loop, as in the case of drawing a horizontal
line from (X1,Y) to (X2,Y):
For X := X1 To X2 Do PutPixel (X, Y, Color) ;
Once the values are known to be within the array bounds,
then the routine can avoid the penalty of clipping inside
every call to PutPixel, as in:
FOR X := X1 To X2 Do Screen_Array[Y, X] := Color ;
Of course, the fastest method is to perform all of the
clipping in the horizontal and vertical line routines,
then use FillChar if possible (Lswap is a utility
routine, necessary for clipping, and the Exit
command immediately returns from the procedure in which
it occurs):
Procedure Lswap (Var N1,N2:LongInt) ;
Var Temp : LongInt ;
Begin
Temp := N1 ;
N1 := N2 ;
N2 := Temp ;
End ;
Procedure Horizontal_Line (X1,X2,Y:LongInt ; Color:Byte) ;
Begin
If Y < Zero Then Exit ;
If Y > GetMaxY Then Exit ;
If X1 > X2 Then Lswap (X1, X2) ;
If X1 < Zero Then X1 := Zero ;
If X2 > GetMaxX Then X2 := GetMaxX ;
If X1 <= X2 Then FillChar (Screen_Array[Y, X1], X2 - X1 + 1, Color) ;
End ;
Procedure Vertical_Line (X,Y1,Y2:LongInt ; Color:Byte) ;
Var Y : Integer ;
Begin
If X < Zero Then Exit ;
If X > GetMaxX Then Exit ;
If Y1 > Y2 Then Lswap (Y1, Y2) ;
If Y1 < Zero Then Y1 := Zero ;
If Y2 > GetMaxY Then Y2 := GetMaxY ;
For Y := Y1 To Y2 Do Screen_Array[Y, X] := Color ;
End ;
At this point, everything else of importance can be implemented
in terms of the routines given above. A general purpose Bresenham
line routine calls PutPixel (or Horizontal_Line or Vertical_Line
if those special conditions are met), normal circles and ellipses
call PutPixel, filled circle and filled rectangle routines call
Horizontal_Line, and so on. Certainly, it is possible to tune
these routines to the VGA hardware, and if speed is an issue (it
always is) the tuning should be done. If, on the other hand,
correctness is more important than tuning (it always is), then
the rectangle, circle, and other specialized routines can and
should be written in terms of the primitives listed here. If
necessary, they can be tuned for speed later.
VGA_320.ZIP (15K)
This is a link to a file containing:
- (13K) A working Turbo Pascal unit with all of the routines listed above
- (11K) The source of a test program, and
- (14K) The MS-DOS executable.
In the source, tabs are assumed to be every 8 spaces.
Back to the CS 32 Page
Back to Dr. Bill's Page