                      ADVANCED PC GRAPHICS WITH C

                            Matthew Probert
                           Servile  Software


This info journal aims to reveal more about the graphics facilities 
offered by the IBM PC, in particular topics which are of a more 
complex nature than those addressed in the previous info journal (#30)



Display Pages

The information for display by the display card is stored in an area 
of memory called the "video RAM". The size of the video RAM varies 
from one display card to another, and the amount of video RAM required 
for a display varies with the selected display mode. Display modes 
which do not require all of the video RAM use the remaining video RAM 
for additional display pages. 


MODE    PAGES
 0        8
 1        8
 2        4 (CGA) 8 (EGA, VGA)
 3        4 (CGA) 8 (EGA, VGA)
 4        1
 5        1
 6        1
 7        8 (EGA, VGA)
13        8 (EGA, VGA)
14        4 (EGA, VGA)
15        2 (EGA, VGA)
16        2 (EGA, VGA)
17        1 (VGA)
18        1 (VGA)
19        1 (VGA)

Many of the BIOS ROM display functions allow selection of the display 
page to be written to, regardless of which page is currently being 
displayed. 

The display card continuously updates the VDU from the information in 
the active display page. Changing the active display page 
instantaneously changes the display. 

This provides the graphics programmer with the means to build a 
display on an undisplayed page, and to then change the active display 
page so that the viewer does not see the display being drawn. 

Selection of the active display page is achieved by calling BIOS ROM 
display function 5 with the number of the required display page stored 
in the assembly language register AL; 

        mov     ah,5
        mov     al,page
        int     10h

Or, from C this function becomes;

#include <dos.h>

void set_page(unsigned char page)
{
    union REGS inreg, outreg;

    inreg.h.ah = 5;
    inreg.h.al = page;
    int86(0x10,&inreg,&outreg);
}

The display page to which BIOS ROM display functions write is decided 
by the value stored in the assembly language BH register. The 
functions for setting a pixel's colour may be amended thus; 

        mov     ah, 12
        mov     al, colour
        mov     bh, page
        mov     cx, x_coord
        mov     dx, y_coord
        int     10h

And the corresponding C function becomes;

#include <dos.h>

void plot(int x_coord, int y_coord, unsigned char colour,
          unsigned char page) 
{ 
    /* Sets the colour of a pixel */

    union REGS inreg, outreg;

    inreg.h.ah = 12;
    inreg.h.al = colour;
    inreg.h.bh = page;
    inreg.x.cx = x_coord;
    inreg.x.dx = y_coord;
    int86(0x10,&inreg,&outreg);
}

The currently active display page can be determined by calling BIOS 
ROM display function 15. This function returns the active display page 
in the assembly language register BH; 

        mov     ah,15
        int     10h
                        ; BH now holds active page number



Advanced Text Routines

When the IBM PC display is in a text mode a blinking cursor is 
displayed at the current cursor position. This cursor is formed of a 
rectangle which is one complete character width, but it's top and 
bottom pixel lines are definable within the limits of the character 
height by calling BIOS ROM display function 1. A CGA display has a 
character height of 8 pixel lines, an EGA display has a character 
height of 14 lines and a VGA display has a character height of 16 
lines. 

BIOS ROM function 1 is called with the top pixel line number of the 
desired cursor shape in assembly language register CH and the bottom 
pixel line number in assembly language register CL; 

        mov     ah,1
        mov     ch,top
        mov     cl,bottom
        int     10h

A C function to set the cursor shape mape be written thus;

#include <dos.h>

void setcursor(unsigned char top, unsigned char bottom)
{
    union REGS inreg, outreg;

    inreg.h.ch = start;
    inreg.h.cl = end;
    inreg.h.ah = 1;
    int86(0x10, &inreg, &outreg);
}

If the top pixel line is defined as larger than the bottom line, then 
the cursor will appear as a pair of parallel rectangles. 

The cursor may be removed from view by calling BIOS ROM display 
function 1 with a value of 32 in the assembly language CH register. 

The current shape of the cursor can be determined by calling BIOS ROM 
function 3, which returns the top pixel line number of the cursor 
shape in the assembly language CH register, and the bottom line number 
in the assembly language register CL. 

Two functions are provided by the BIOS ROM for scrolling of the 
currently active display page. These are function 6, which scrolls the 
display up and function 7 which scrolls the display down. 

Both functions accept the same parameters, these being the number of 
lines to scroll in the assembly language register AL, the colour 
attribute for the resulting blank line in the assembly language BH 
register, the top row of the area to be scrolled in the assembly 
language CH register, the left column of the area to be scrolled in 
the assembly language CL register, the bottom row to be scrolled in 
the assembly language DH register and the right most column to be 
scrolled in the assembly language DL register. 

If the number of lines being scrolled is greater than the number of 
lines in the specified area, then the result is to clear the specified 
area, filling it with spaces in the attribute specified in the 
assembly language BH register. 

A C function to scroll the entire screen down one line can be written 
thus; 

#include <dos.h>

void scroll_down(unsigned char attr)
{
    union REGS inreg, outreg;

    inreg.h.al = 1;
    inreg.h.cl = 0;
    inreg.h.ch = 0;
    inreg.h.dl = 79;
    inreg.h.dh = 24;    /* Assuming a 25 line display */
    inreg.h.bh = attr;
    inreg.h.ah = 7;
    int86(0x10, &inreg, &outreg);
}

A simple clear screen function can be written in C based upon the 
"scroll_down()" function simply by changing the value assigned to 
inreg.h.al to 0; 

#include <dos.h>

void cls(unsigned char attr)
{
    union REGS inreg, outreg;

    inreg.h.al = 0;
    inreg.h.cl = 0;
    inreg.h.ch = 0;
    inreg.h.dl = 79;
    inreg.h.dh = 24;    /* Assuming a 25 line display */
    inreg.h.bh = attr;
    inreg.h.ah = 7;
    int86(0x10, &inreg, &outreg);
}


Windowing functions need to preserve the display they overwrite, and 
restore it when the window is removed from display. The BIOS ROM 
provides a display function which enables this to be done. 

Function 8 requires the appropriate display page number to be stored 
in assembly language register BH, and then when called it returns the 
ascii code of the character at the current cursor position of that 
display page in the assembly language AL register, and the display 
attribute of the character in the assembly language AH register. 

The following C functions allow an area of the display to be 
preserved, and later restored; 

#include <dos.h>

void at(unsigned char row, unsigned char column, unsigned char page)
{
    /* Position the cursor */

    union REGS inreg,outreg;

    inreg.h.ah = 2;
    inreg.h.bh = page;
    inreg.h.dh = row;
    inreg.h.dl = column;
    int86(0x10,&inreg,&outreg);
}


void get_win(unsigned char left, unsigned char top, unsigned char right,
            unsigned char bottom, unsigned char page, char *buffer)
{
    /* Read a text window into a variable */

    union REGS inreg,outreg;

    unsigned char old_left;
    unsigned char old_row;
    unsigned char old_col;

    /* save current cursor position */
    inreg.h.ah = 3;
    inreg.h.bh = page;
    int86(0x10,&inreg,&outreg);
    old_row = outreg.h.dh;
    old_col = outreg.h.dl;

    while(top <= bottom)
    {
        old_left = left;
        while(left <= right)
        {
            at(top,left,page);
            inreg.h.bh = page;
            inreg.h.ah = 8;
            int86(0x10,&inreg,&outreg);
            *buffer++ = outreg.h.al;
            *buffer++ = outreg.h.ah;
            left++;
        }
        left = old_left;
        top++;
    }

    /* Restore cursor to original location */
    at(old_row,old_col,page);
}

void put_win(unsigned char left, unsigned char top, unsigned char right, 
             unsigned char bottom, unsigned char page, char *buffer)
{
    /* Display a text window from a variable */

    union REGS inreg,outreg;

    unsigned char old_left;
    unsigned char chr;
    unsigned char attr;
    unsigned char old_row;
    unsigned char old_col;

    /* save current cursor position */
    inreg.h.ah = 3;
    inreg.h.bh = page;
    int86(0x10,&inreg,&outreg);
    old_row = outreg.h.dh;
    old_col = outreg.h.dl;

    while(top <= bottom)
    {
        old_left = left;
        while(left <= right)
        {
            at(top,left,page);
            chr = *buffer++;
            attr = *buffer++;
            inreg.h.bh = page;
            inreg.h.ah = 9;
            inreg.h.al = chr;
            inreg.h.bl = attr;
            inreg.x.cx = 1;
            int86(0x10,&inreg,&outreg);
            left++;
        }
        left = old_left;
        top++;
    }

    /* Restore cursor to original location */
    at(old_row,old_col,page);
}

DIRECT VIDEO ACCESS WITH THE IBM PC

Accessing video RAM directly is much faster than using the BIOS ROM 
display functions. There are problems however. Different video modes 
arrange their use of video RAM in different ways so a number of 
functions are required for plotting using direct video access, where 
as only one function is required if use is made of the BIOS ROM 
display function. 

The following C function will set a pixel in CGA display modes 4 and 5 
directly;

void dplot4(int y, int x, int colour)
{
    /* Direct plotting in modes 4 & 5 ONLY! */

    union mask
    {
        char c[2];
        int i;
    }bit_mask;

    int index;
    int bit_position;

    unsigned char t;
    char xor;

    char far *ptr = (char far *) 0xB8000000;

    bit_mask.i = 0xFF3F;

    if ( y < 0 || y > 319 || x < 0 || x > 199)
        return;

    xor = colour & 128;

    colour = colour & 127;

    bit_position = y % 4;

    colour <<= 2 * (3 - bit_position);

    bit_mask.i >>= 2 * bit_position;

    index = x * 40 + (y / 4);

    if (x % 2)
        index += 8152;

    if (!xor)
    {
        t = *(ptr + index) & bit_mask.c[0];
        *(ptr + index) = t | colour;
    }
    else
    {
        t = *(ptr + index) | (char)0;
        *(ptr + index) = t ^ colour;
    }
}

Direct plotting in VGA mode 19 is very much simpler;

void dplot19(int x, int y, unsigned char colour)
{
    /* Direct plot in mode 19 ONLY */
    char far *video;

    video = MK_FP(0xA000,0);
    video[x + y * 320] = colour;
}

INCREASING COLOURS

The EGA display is limited displaying a maximum of 16 colours, however 
in high resolution graphics modes (such as mode 16) the small physical 
size of the pixels allows blending of adjacent colours to produce 
additional shades. 

If a line is drawn straight across a black background display in blue, 
and then a subsequent line is drawn beneath it also in blue but only 
plotting alternate pixels, the second line will appear in a darker 
shade of the same colour. 

The following C program illustrates this idea;

#include <dos.h>

union REGS inreg, outreg;

void setvideo(unsigned char mode)
{
    /* Sets the video display mode */

    inreg.h.al = mode;
    inreg.h.ah = 0;
    int86(0x10, &inreg, &outreg);
}

void plot(int x, int y, unsigned char colour)
{
    /* Sets a pixel at the specified coordinates */

    inreg.h.al = colour;
    inreg.h.bh = 0;
    inreg.x.cx = x;
    inreg.x.dx = y;
    inreg.h.ah = 0x0C;
    int86(0x10, &inreg, &outreg);
}

void line(int a, int b, int c, int d, unsigned char colour)
{
    /* Draws a straight line from (a,b) to (c,d) */

    int u;
    int v;
    int d1x;
    int d1y;
    int d2x;
    int d2y;
    int m;
    int n;
    int s;
    int i;

    u = c - a;
    v = d - b;
    if (u == 0)
    {
        d1x = 0;
        m = 0;
    }
    else
    {
        m = abs(u);
        if (u < 0)
            d1x = -1;
        else
            if (u > 0)
                d1x = 1;
    }
    if ( v == 0)
    {
        d1y = 0;
        n = 0;
    }
    else
    {
        n = abs(v);
        if (v < 0)
            d1y = -1;
        else
            if (v > 0)
                d1y = 1;
    }
    if (m > n)
    {
        d2x = d1x;
        d2y = 0;
    }
    else
    {
        d2x = 0;
        d2y = d1y;
        m = n;
        n = abs(u);
    }
    s = m / 2;

    for (i = 0; i <= m; i++)
    {
        plot(a,b,colour);
        s += n;
        if (s >= m)
        {
            s -= m;
            a += d1x;
            b += d1y;
        }
        else
        {
            a += d2x;
            b += d2y;
        }
    }
}

void dot_line(int a, int b, int c, int d, int colour)
{
    /* Draws a dotted straight line from (a,b) to (c,d) */

    int u;
    int v;
    int d1x;
    int d1y;
    int d2x;
    int d2y;
    int m;
    int n;
    int s;
    int i;

    u = c - a;
    v = d - b;
    if (u == 0)
    {
        d1x = 0;
        m = 0;
    }
    else
    {
        m = abs(u);
        if (u < 0)
            d1x = -2;
        else
            if (u > 0)
                d1x = 2;
    }
    if (v == 0)
    {
        d1y = 0;
        n = 0;
    }
    else
    {
        n = abs(v);
        if (v < 0)
            d1y = -2;
        else
            if (v > 0)
                d1y = 2;
    }
    if (m > n)
    {
        d2x = d1x;
        d2y = 0;
    }
    else
    {
        d2x = 0;
        d2y = d1y;
        m = n;
        n = abs(u);
    }
    s = m / 2;

    for (i = 0; i <= m; i+=2)
    {
        plot(a,b,colour);
        s += n;
        if (s >= m)
        {
            s -= m;
            a += d1x;
            b += d1y;
        }
        else
        {
            a += d2x;
            b += d2y;
        }
    }
}


void main(void)
{
    int n;

    /* Display different colour bands */

    setvideo(16);

    for(n = 1; n < 16; n++)
    {
        line(0,n * 20,639,n * 20,n);
        line(0,1 + n * 20,639,1 + n * 20,n);
        line(0,2 + n * 20,639,2 + n * 20,n);

        dot_line(0,4 + n * 20,639,4 + n * 20,n);
        dot_line(1,5 + n * 20,639,5 + n * 20,n);
        dot_line(0,6 + n * 20,639,6 + n * 20,n);

        dot_line(0,8 + n * 20,639,8 + n * 20,n);
        dot_line(1,9 + n * 20,639,9 + n * 20,n);
        dot_line(0,10 + n * 20,639,10 + n * 20,n);

        dot_line(1,8 + n * 20,639,8 + n * 20,7);
        dot_line(0,9 + n * 20,639,9 + n * 20,7);
        dot_line(1,10 + n * 20,639,10 + n * 20,7);

        dot_line(1,12 + n * 20,639,12 + n * 20,n);
        dot_line(0,13 + n * 20,639,13 + n * 20,n);
        dot_line(1,14 + n * 20,639,14 + n * 20,n);

        dot_line(0,12 + n * 20,639,12 + n * 20,14);
        dot_line(1,13 + n * 20,639,13 + n * 20,14);
        dot_line(0,14 + n * 20,639,14 + n * 20,14);
    }
}

This technique can be put to good use for drawing three dimensional 
boxes; 

void box3d(int xa,int ya, int xb, int yb, int col)
{
    /* Draws a box for use in 3d histogram graphs etc */

    int xc;
    int xd;

    int n;

    xd = (xb - xa) / 2;
    xc = xa + xd;

    /* First draw the solid face */
    for(n = xa; n < xb; n++)
        line(n,ya,n,yb,col);

    /* Now "shaded" top and side */
    for(n = 0; n < xd; n++)
    {
        dotline(xa + n,yb - n ,xc + n,yb - n,col);
        dotline(xa + xd + n,yb - n ,xc + xd + n,yb - n,col);
        dotline(xb +n ,ya - n ,xb + n,yb - n,col);
    }
}


DISPLAYING TEXT AT PIXEL COORDINATES

When using graphics display modes it is useful to be able to display 
text not at the fixed character boundaries, but at pixel coordinates. 
This can be achieved by implementing a print function which reads the 
character definition data from the BIOS ROM and uses it to plot pixels 
to create the shapes of the desired text. The following C function, 
"gr_print()" illustrates this idea using the ROM CGA (8x8) character 
set; 

void gr_print(char *output, int x, int y, unsigned char colour)
{
    unsigned char far *ptr;
    unsigned char chr;
    unsigned char bmask;
    int i;
    int k;
    int oldy;
    int p;
    int height;

    /* The height of the characters in the font being accessed */
    height = 8;

    /* Set pointer to start of font definition in the ROM */
    ptr = getfontptr(3);

    oldy = y;
    p = 0;

    /* Loop output string */
    while(output[p])
    {
        /* Get first character to be displayed */
        chr = output[p];

        /* Loop pixel lines in character definition */
        for(i = 0; i < height; i++)
        {
            /* Get pixel line definition from the ROM */
            bmask = *(ptr + (chr * height) + i);

            /* Loop pixel columns */
            for (k = 0; k < 8; ++k, bmask <<= 1)
            {
                /* Test for a set bit */
                if(bmask & 128)
                    /* Plot a pixel if appropriate */
                    plot(x,y,colour);
                else
                    plot(x,y,0);
                x++;
            }
            /* Down to next row and left to start of character */
            y++;
            x -= 8;
        }
        /* Next character to be displayed */ 
        p++;

        /* Back to top row of the display position */
        y = oldy;

        /* Right to next character display position */
        x += 8;
    }
}

The following assembly language support function is required to 
retrieve the address of the ROM font by calling the BIOS ROM display 
function which will return the address; 

; GET FONT POINTER
; Small memory model
; compile with tasm /mx

_TEXT    segment    byte public 'CODE'
         assume    cs:_TEXT,ds:NOTHING

_getfontptr    proc    near
               push    bp
               mov     bp,sp
               mov     ax,1130h
               mov     bh, [bp+4]        ; Number for font to be retrieved
               int     10h
               mov     dx,es
               mov     ax,bp
               pop     bp
               ret
_getfontptr    endp

_TEXT    ends

    public    _getfontptr
    end

The font number supplied to "getfontptr()" can be one of;

    2    ROM EGA 8 x 14 font
    3    ROM CGA 8 x 8 font
    6    ROM VGA 8 x 16 font


