Fast VGA Graphics Without BGI

by Bas van Gaalen [copyright 1995]

Many people are interested in the subject of fast graphics using Pascal, combined with assembler where necessary. This article is a reply to that interest. The use of assembler will be kept to a minimum, although it might be clear that in most cases assembler will be a good way of obtaining more speed. However, building an efficient data structure in Pascal and eliminating bottle-necks is the first step for optimisation (as you will of course know, if you've read Dr Bob's articles).

Initialisation

The first thing to do is choose a graphics mode. For this article we will use graphics mode 13h (decimal 19) which corresponds to a resolution of 320 pixels horizontally and 200 pixels vertically, making a total of 64000 pixels on one page. Mode 13h is a chained 8-bit mode. Chained means that the four bit planes are addressed automatically. It is possible to 'unchain' this mode and thus create a so-called mode-x. But that's another story, which we'll maybe embark on later.

8-bit means that 8 bits represent one color (actually a color-index). Since 8 bits form a byte we have 256 colors at our disposal and each byte corresponds exactly with one pixel on the screen.

So, we therefore have a linear addressed memory block (unlike mode 12h, which is a planar mode). This makes mode 13h very easy for 'direct-access-programming' (which simply means that you write the data directly to the video memory, without calling any interrupts).

To initialise this mode we make use of an interrupt-call. This can be done using the registers in the DOS unit or with three lines of built-in assembler (BASM) code, as shown in Listing 1.

Listing 1: Initialising mode 13h procedure setvideo(md:byte); assembler; asm mov ah, O mov al, md int lOh end;

Addressing The Memory

There are basically three ways of addressing the graphics memory: using interrupts (the slowest), calling BGI-functions (if you have initialised the graphics mode with initgraph from the GRAPH unit) and using direct memory access (the fastest method). Listing 2 shows how method one is accomplished. How BGI works is explained in the Turbo Pascal manuals, so I will not go into that in this article. The last method is far more interesting for the speed gain it provides in comparison with the other two methods.

Listing 2: Interrupt call to display a single pixel procedure int_putpixel(column,row:word; color:byte); assembler; asm mov ah, Och mov al, color mov bh, O mov cx, column mov dx, row int lOh end;

The VGA graphics memory is located at segment $aOOO in the first 1Mb of memory, just beyond the 640Kb conventional memory limit. 64 Kb of this memory can be addressed directly, without the need for 'bank-switching'. Since mode 13h is only 64000 bytes, it will fit just fine; we even have 1536 spare bytes (which is 4.8 screen lines).

Picture the graphics-memory as a grid of 320 bytes horizontal (0-319) and 200 bytes vertical (0-199). The upper left corner represents x = O, y = O, and thus the total offset will be O. Now, say we want to plot a pixel at x = 319 and y = 199 (the bottom right corner of the screen). We want to find the offset at which the pixel resides in memory. This can be done by multiplying the y-coordinate by the width of the screen and adding the x-coordinate. Or:

offset = (y * screen_width) + x

The width of the screen is 320 in this mode, so this corresponds with:

offset = (y * 320) + x

The general formula will work for any linear video mode, including SuperVGA modes.

Listing 3a shows a put-pixel routine in Pascal. This is, however, not the best way. It is better to declare the video segment as a global constant, so it isn't initialised every time a pixel is written (this slows down the process). The code in this example isn't the fastest either. In fact, it's possibly the slowest. Try Listing 3b for a very fast alternative. As this is assembler, the video segment has to be assigned every time the procedure is called, since the register ES might change (it probably will).

Addressing the memory like this may run into conflicts with DPMI. So, if you are using Turbo/Borland Pascal, use SegAOOO instead of $aOOO in Listing 3a.

Listing 3a: Direct memory access - Pascal method to display a single pixel procedure pas_putpixelCcolumn. row: word; color: byte); var video_segment, video_ofset : word; begin video_segment := $aOOO; video_ofset := row * 320 + column; mem[video_segment:video_ofset] := color; end; Listing 3b: Direct memory access - in/ine method to display a single pixel procedure putpixel(c : byte; x, y : integer); inline( $b8/$00/$aO/ { mov ax, $aOOO } $8e/$cO/ { mov es, ax } $5b/ { pop bx } $88/$dc/ { mov ah, bl } $5f/ ( pop di ) $0l/$c7/ { add di, ax } {$ifopt g+} $cl/$e8/$02/ { shr ax, 2 } {$else} $dl/$e8/ { shr ax, 1 } $dl/$e8/ { shr ax, 1 } {$endif} $01/$c7/ { add di, ax } $58/ { pop ax } $aa); { stosb }

Defining The Colors

Standard VGA has 256 colors. Each color is represented by the intensities of red, green and blue (rgb). These intensities are 6-bit, so they range from 0 to 63: 0 is black and 63 is the brightest intensity. Knowing this, we see that we can choose from a total palette of 262144 colors (256Kb = 64 * 64 * 64). With the rgb values one can produce composite colors, for instance: black = 0,0,0 bright white = 63,63,63 bright blue = 0,0,63 etc ... Every pixel on the screen actually maps to an rgb value of the current palette; it serves as an index.

If the VGA card finds the value 15 at offset 0, it will search the current palette (which resides in the memory of the video card and not in the base memory: a common mistake) for the corresponding rgb values, which will then be sent to the monitor. In this case 15 represents bright white in the standard palette (rgb: 42,42,63).

To manipulate these values one can use (again) the same three methods. Interrupts and BGI are slow (BGI actually takes several seconds to set a complete palette of 256 colors), while the direct video card access is much faster (it takes only a few microseconds to set a complete palette). The interrupt method is shown in Listing 4.

Listing 4: Interrupt call to alter rgb values of a color procedure int_setrgb(color : word; r, g, b : byte); assembler; begin mov ah, 10h mov bx, col mov ch, g mov cl, b mov dh, r int 10h end;

The PC has a series of ports to communicate with the hardware on the ports or extension slots, like a printer, modem or video card. In Pascal you can access these ports using the port array. In assembler the in/out statements are meant for reading and writing data from or to the ports. Ports are often also called registers. I will use both terms.

There are control registers (to send commands) and data registers (to send or receive data). In general:

data register = control register + 1

Back to the video card. The general approach is:

The control register for writing the rgb values is 03c8h (968 decimal). The accompanying data register is (thus) 03c9h. What you do is feed register 03c8h with the color index you want to alter and then send the three rgb values to register 03c9h. The palette change is activated at the third (actually every third) send to 03c9h. When this occurs, the index in register 03c8h will automatically be increased. Check Listing 5 for the procedure.

Listing 5: Direct video card writes to alter rgb values of a color procedure pas_setrgb(color: word; red, green, blue: byte); begin port[$03c8] := color; port[$03c9] := red; port[$03c9] := green; port[$03c9] := blue; end;

Reading the rgb values works pretty much the same, except you send the color index to register 03c7h, and you read the rgb values from register 03c9h instead of writing them.

In Listing 6 is a proportional fade routine, which can be built with the knowledge you have received from this article. Of course all the listings are on the disk with this issue as well.

Listing 6: Proportional fade routines to fade pal1 to pal2 type pal_type = array[O..255] of record r, g, b : byte; end procedure setpal(pal : pointer); assembler; asm push ds mov dx, 03c8h xor al, al out dx, al inc dx mov cx, 0300h lds si, pal cld rep outsb pop ds end; procedure vretrace; assembler; asm mov dx, 3dah @vertl: in al, dx test al, 8 jz @vertl @vert2: in al, dx test al, 8 jnz @vert2 end; procedure fadepal(pall, pa12 : pal_type; iO, i1, step: byte); var dpal, wpal : array[O..255] of record r, g, b : real; end; i, j : byte; begin for i := iO to il do begin dpal[i].r := (pal2[i].r - pall[i].r) / step; dpal[i].g := (pal2[i].g - pall[i].g) / step; dpal[i].b := (pal2[i].b - pall[i].b) / step; wpal[i].r := pal1[i].r; wpal[i].g := pal1[i].g; wpal[i].b := pall[i].b; end; for j := 1 to step do begin vretrace; for i := iO to il do begin wpal[i].r := wpal[i].r + dpal[i].r; pal1[i].r := round(wpal[i].r); wpal[i].g := wpal[i].g + dpal[i].g; pall[i].g := round(wpal[i].g); wpal[i].b := wpal[i].b + dpal[i].b; pall[i].b := round(wpal[i].b); end; setpal(@pall); end; end;

Bas van Gaalen lives in Eindhoven in The Netherlands. He can be contacted on the Internet as bas@il.ft.hse.nl or on Fidonet at 2:284/123.2 or also on Pascalnet at 115:3145/103.2 [addresses not valid anymore!]