Addressing Super VGA Modes From Protected Mode C

Gary R. Olhoeft


Gary R. Olhoeft has been programming digital computers since 1968. He has a BSEE, MSEE and PhD in physics. Inquiries about the complete libhpgl.lib extended graphics library may be addressed to Gary R. Olhoeft, P.O. Box 10870 Edgemont, Golden, CO 80401; Compuserve 76665,2021, or golhoeft on BIX.

Many compilers lack support for the Super VGA modes available on some graphics cards. This article and the accompanying code explain the pixel-level addressing requirements of one card, the ATI VGA Wonder, in modes from EGA up to 800x600x256 and 1024x768x16 from the 80386 protected mode. The code was developed with MicroWay's NDP C-386 compiler, running under Phar Lap's protected mode extensions to MS-DOS, as part of a larger device independent graphics library. The ability to address four gigabytes of linear memory and other advantages of 386 protected mode extensions to DOS are discussed in Ducan (1990).

Pixel Addressing

Low-level pixel addressing cannot be accomplished with ANSI C constructs alone. At some point, C extensions are necessary to invoke interrupts, address ports, and read or write graphics card registers or memory. These operations may all readily be done in assembly language, but assembly language is very hardware-specific and not portable. The first version of the code presented here was developed using the blk_bm(), blk_mb(), inp(), int386() and outp() extended functions in the libGREX.lib of MicroWay's NDP C-386 compiler. The MicroWay manual states: "These routines allow you to write to I/O ports, access memory directly, and pass control to interrupt service routines. Used improperly, they will almost certainly lead to disaster. Under most circumstances, you should not have to use these functions. When they are used, these routines should be called with extra care." Taking extra care, these are exactly the functions required for pixel-level graphics drivers. After development and testing with these extended functions, critical functions were recoded with the Phar Lap 386 | ASM assembler. Then, the assembler output of the compiler was studied to locate the time-sensitive portions where the C and assembler code could best be merged. The final products were functions coded in NDP C-386 using the inline assembler (a portion of which are presented here).

ATI provides (with the card) a number of assembly language code fragments to assist program development. Developing protected mode code required solving several problems, only one of which is discussed here: addressing the graphics card registers and memory in real mode from protected mode. The code was developed in a mixture of C and inline assembler as a trade-off between the portability of C and the speed of assembler. Inline assembly keeps all the code in one place without separate compile, assemblage, and link steps. This is a significant advantage when supporting more than one graphics card, each with separate assembly-level SVGA driver requirements.

MicroWay suggests addressing real mode memory through the blk_mb() and blk_bm() functions. I wanted more selective control at the byte, word and double word level, so I developed the code to address real mode memory more directly. An example is the poke() function that puts a single byte into real mode memory:

#define   poke(addr,  val) asm("  push  es");\
asm("     mov    ax,  034h");\
asm("     mov    es,  ax");\
ebx  =   addr;  cl =  val;\
asm(ebx,   cl,  "    mov  byte  ptr  es:[ebx],  cl");\
asm("     pop    es")
implemented as a #define using inline assembler. A commented version appears in Listing 2. Listing 1 also shows the peek() function.

Listing 3 shows how to substitute and poke an 8-bit byte, 16-bit word or 32-bit double word into real memory from protected mode.

The simplest example is illustrated with the 256 color MCGA mode 0x13 320x200 that uses one byte per pixel. To write color (0-255) to pixel (x,y), the code is:

poke(655360 + y*colx + x, color);
/*  colx = 320 pixels across the screen */
where x and y are pixel screen coordinates; 0, 0 at upper left.

Not quite as simple, but given as examples in several texts, the 16-color EGA modes (0x10 640x350, 0x12 640x480 and 0x54 800x600) are addressed through the EGA registers (where colx is 640 or 800) (Listing 4) .

Still more complicated are the higher color or resolution 640x480x256, 800x600x256, and 1024x768x16 modes. The real mode graphics memory starting at address 655360 (0xA000000) only stretches for 64K (65536) bytes in size. Yet these modes require 307,200 (640x480), 480,000 (800x600), or 393,216 (1024x768) bytes (the last is two pixels per byte) to fill a graphics screen. Addressing the graphics memory in the graphics card requires sending the data through the 64K region starting at real memory address 655360. Sending through more than 64K requires breaking the data into 64K blocks and paging the graphics card. Going through the same exercise as before, Listing 5 shows the code to write color (0-255) in 640x480 or 800x600 256-color mode to x,y.

The variable old_page is used to determine whether the current and previous coordinates are on the same or different memory pages. After the paging code, note the similarity to the 320x200x256 mode. Listing 6 gives the code to write color (0-15) in 1024x768 mode to x,y.

However, in this version the code changes to accommodate two pixels per byte. Since only one pixel is changing, the byte containing both neighboring current pixels must be read in, it must be determined which half of the byte to change, and both (original and changed) pixels must be written back out as one byte.

As shown, these code fragments plot a single pixel at a time. If a group of pixels are to be plotted at once (as a series of points defining a line vector or plane image), the code can be optimized. For example, in raster plotting in the 1024x768 mode, paging is only important when y is changing, not when x is changing. Similarly in 256 color mode, instead of writing one byte at a time, four pixels along the x-axis can be combined into one 32-bit double word that can be moved much faster than four individual bytes. On a 25-MHz 80386 using raster methods on the ATI VGA Wonder, the entire screen can be filled with an image in 0.17 second (320x200x256 MCGA) to 0.83 second (1024x768x16 SVGA) to 4.28 seconds (640x480x16 VGA).

The remainder of Listing 1 shows how to open graphics modes and generalize a pixel write for any mode (mode independent graphics). The code also shows the differences between setting the color palettes in the various modes. All of the 256-color modes use the vga_palette() function, and the 16-color modes (except 1024x768) use the set_palette() function from the MicroWay libGREX. lib. The 1024x768x16 mode uses the ext_palette() function shown in the listing.

The complete library uses this type of approach to provide device independent graphics for several graphics cards of different manufacturers, with windowing and global user scaling (instead of pixel counting), WYSIWYG CRT vector graphics to HPGL plotters/printers, rotatable and scalable fonts, mixed vector and raster graphics, and more. Only four library functions (the vector pixel driver, the raster image driver, the graphics mode function, and the window clear function) need modification to add capabilities of new graphics cards. Most graphics cards will obey the EGA 16-color register modes, with allowances made for the differing mode numbers (for example, the 800x600x16 mode on the ATI is 0x63, but the Orchid is 0x30 and Paradise is 0x58). Most graphics cards set the video mode much like the standard IBM modes (using BIOS interrupt 10h and function 00h) as shown in the listing, but some (for example, Everex and Video 7) set the video mode differently. The 1024x768x16 modes may or may not be paged. The 256-color modes use different memory paging boundaries and addresses, requiring separate treatments for each card beyond the IBM standard MCGA 320x200x256 (for example, ATI uses 64K pages but Paradise uses 4K pages). The Video Electronics Standards Association (VESA) is attempting to standardize these super VGA graphics modes, and VESA compatible cards are now available. The IBM 8514/A and Texas Instruments 340x0 TIGA graphics interfaces are different due to the on board graphics co-processor. However, the same types of problems addressed in these examples will also guide the solution to driving those graphics boards (although some, such as the Truevision ATVista 34010 board, may be addressed directly from protected mode).

Suggested reading:

Duncan, Ray, ed., 1990, Extending DOS, Reading, MA, Addison-Wesley, 432p.

Ericson, Bo, 1990, "VESA VGA BIOS Extensions," Dr. Dobb's Journal, v.14, n.4, p.65-70.

Kliewer, Bradley D., 1988, EGA/VGA A Programmer's Reference Guide, NY, McGraw-Hill, 269p.

Richter, Jake and Smith, Bud, 1990, Graphics Programming For The 8514/A, Redwood City, CA, M&T Books, 366p.

Stevens, Roger T., 1988, Graphics Programming In C, Redwood City, CA, M&T Publishing Inc., 639p.

Listing 1

/*  Copyright 1990 by Gary R. Olhoeft
 *  This code may be freely copied for personal,
 *  non-commercial use. compile using MicroWay's
 *  NDP C-386 compiler:
 *    cc graphics.c -w -v -rt2 -n2 -n3 -lGREX.LIB
 */
#include <stdio.h>
#include <dos.h>
#include <grex.h>
void plot_pixel();
void graphics();
void spectrum();
void ext_palette();

union REGS reg;    /*  required by inline assembly */
int v_mode, v_color, v_rowy, v_colx, page;
unsigned short ati_extreg;
char ega_palette[17] = (0,1,9,11,3,19,2,18,6,54,38,
                    52,4,36,41,13,0};
int modes[] = {0x0d,0x0e,0x10,0x12,0x13,0x54,
             0x62,0x63,0x65};
void main()
{
  int i,x,y,key;
  unsigned char color;
  for (i=0; i<9; i++)
    {
      graphics(modes[i]); /*  cycle through ATI VGA
                        Wonder graphics modes */
      printf("Mode %d %dx%dx%d\n",v_mode,v_colx+1,
              v_rowy+1,v_color+1);
      /* printing shrinks as pixel count increases */
      spectrum();
      /* draw banded color lines on screen */
      for (x = 0; x<=v_colx>>2; x++)
        {
          for (y = 0; y<=v_rowy; y++)
            {
              color = y % v_color;
              plot_pixel(x+y,y,color);
            }
        }
      /*  pause loop until keypress;
         exit with ^C or Esc */
      if ((key=pauseb())==27) {
         set_video_mode(0x02); exit();
         }
      }
  set_video_mode(0x02); /* restore text mode */
  exit();
}
void graphics(mode)
unsigned char mode;
{
  unsigned char buffer[2];
  switch (mode) /*  note: not all possible modes shown */
    {
      case 0x0d: /*  EGA */
         v_colx = 320;
         v_rowy = 200;
         v_color = 16;
         break;
      case 0x0e: /*  EGA */
         v_colx = 640;
         v_rowy = 200;
         v_color= 16;
         break;
      case 0x10: /*  EGA high resolution */
         v_colx = 640;
         v_rowy = 350;
         v_color = 16;
         break;
      case 0x12: /*  VGA */
         v_colx = 640;
         v_rowy = 480;
         v_color = 16;
         break;
      case 0x13: /*  MCGA */
         v_colx = 320;
         v_rowy = 200;
         v_color = 256;
         break;
      case 0x54: /*  ATI SVGA */
         v_colx = 800;
         v_rowy = 600;
         v_color = 16;
         break;
      case 0x62: /*  ATI SVGA */
         v_colx = 640;
         v_rowy = 480;
         v_color = 256;
         break;
      case 0x63: /*  ATI SVGA */
         v_colx = 800;
         v_rowy = 600;
         v_color = 256;
         break;
      case 0x65: /*  ATI SVGA */
         v_colx = 1024;
         v_rowy = 768;
         v_color = 16;
         break;
      default:
         mode = 0x02; /*  text mode */
         v_colx = 80;
         v_rowy = 25;
         v_color = 1;
         break;
    }
    v_mode = mode;
    v_colx--; /*  set ranges 0 to pixels-1 */
    v_rowy--;
    v_color--;
    
    reg.b.ah = 0;
    reg.b.al = mode;
    int386(0x10,&reg,&reg); /* call BIOS video
                     interrupt to set mode */
    
    blk_mb(buffer, 0x34, 786448, 2);
    ati_extreg = buffer[0]+256*buffer[1];
        /*  find ATI extended_reg address
               (could use peek() instead)*/
    page = 99; /*  force paging first plot cycle */
}

/*  plot pixel of color at (x,y) display units */
void plot_pixel(x,y, color)
int x,y;
unsigned char color;
{
#define peek(addr, val) asm("     push    es"); \
       asm("     mov   ax, 034h"); asm("     mov  \
       es, ax"); ebx = addr;  asm(ebx, "    mov   \
       cl, byte ptr  es:[ebx]", cl); val = cl; asm\
       ("     pop    es")
#define poke(addr, val)  asm("      push   es"); \
       cl = val; asm("      mov    ax, 034h"); \
       asm("     mov    es, ax");  ebx = addr;  \
       asm(ebx, cl,  "     mov   byte ptr es:[ebx], \
       cl"); asm("      pop   es")
/*  required by inline assembly */
reg$eax unsigned short ax;
reg$eax unsigned char al;
reg$ah unsigned char ah;
reg$ecx unsigned short cx;
reg$ch unsigned char ch;
reg$ecx unsigned char cl;
reg$edx unsigned short dx;
reg$edx unsigned char dl, val;
reg$ebx unsigned ebx, addr;
int i,vcol,yvx;
if ((y < 0) || (y > v_rowy) || (x < 0) ||
   (x > v_colx)) return;
     /* clip physical display boundaries */
y = v_rowy-y; /* put (0,0) at lower left corner of plotter */
vcol = v_colx+1;
yvx = y*vcol + x;
switch (v_mode)
  {
    case 0x65: /* ATI 1024x768x16 */
      ch = (char)(y >> 6);
      if (ch!=page) /* only change page if different */
        {
          dx = ati_extreg; /* location of ATI card external register */
          asm("     cli     ");         /*  disable interrupts */
          asm("     mov     al,0b2h");  /*  page select */
       asm(dx,"      out     dx,al");    /*  ATI extended register */
          asm("     inc     dl");
          asm("     in      al,dx");
          asm("     mov     ah,al");
          asm("     and     ah,0e1h");  /*  page mask */
       asm(ch,"      or      ah, ch");   /*  ch = memory page desired */
          asm("     mov     al,0b2h");  /*  page select */
          asm("     dec     dl");
          asm("     out     dx,ax");
          asm("     sti     ");         /*  enable interrupts */
          page = ch;
        }
      addr = 655360 + ((y << 9) % 65536) + (x >> 1);
      peek(addr, val); /* read existing color of pixel pair */
      if (x % 2) val = color | (val & 0xF0); /* change addressed pixel */
      else val = (color << 4) | (val & 0x0F);
      poke(addr, val); /* write pixel pair */
      break;
    case 0x13: /*  MCGA 320x200x256 */
      addr = 655360 + yvx;
      poke(addr, color); /* write direct to real video memory */
      break;
    case 0x62: /* ATI 640x480x256 */
    case 0x63: /* ATI 800x600x256 */
      ch = (unsigned char)(yvx >> 15);
      if (ch!=page)
        {
          dx = ati_extreg; /* location of ATI card external register */
          asm("     cli     ");         /*  disable interrupts */
          asm("     mov     al,0b2h");  /*  page select */
       asm(dx,"      out     dx,al");    /*  ATI extended register */
          asm("     inc     dl");
          asm("     in      al,dx");
          asm("     mov     ah,al");
          asm("     and     ah,0e1h");  /*  page mask */
       asm(ch,"      or      ah,ch");    /*  ch = memory page desired */
          asm("     mov     al,0b2h");  /*  page select */
          asm("     dec     dl");
          asm("     out     dx,ax");
          asm("     sti     ");         /*  enable interrupts */
          page = ch;
        }
      addr = 655360 + (yvx % 65536);
      poke(addr, color); /* write direct to real video memory */
      break;
    default:
    /  * 0x10 EGA 640x350x16, 0x12 VGA 640x480x16, 0x54 ATI 800x600x16 */
        asm("     push   es");        /*  save segment register */
        asm("     mov    ax, 034h");  /*  use Phar Lap LDT to */
        asm("     mov    es, ax");    /*  access real memory */
        dx = 0x3ce;                   /*  EGA graphics register */
        ebx= 655360 + (yvx >> 3);     /*  memory position of pixel */
    asm(ebx,"      mov    cl, byte ptr es:[ebx]"); */  load EGA registers */
        ax = color << 8;
    asm(dx, ax, "      out    dx, ax"); /*  set color */
           ax = 0x0F01;
    asm(dx, ax,"       out    dx, ax"); /*  enable */
           ax = 0x0003; /* 0x00 = replace, 0x10 OR, 0x18 XOR, 0x08 AND */
    asm(dx, ax,"       out    dx, ax"); /*  pixel write mode */
           ax = ((0x80 >> (x%8)) << 8 ) + 8;
    asm(dx, ax,"       out    dx, ax"); /*  bit mask (8 pixels/byte) */
       asm(ebx,"     mov    byte ptr es:[ebx], 255"); /* write EGA regs */
           asm("         pop    es");     /*  restore segment register */
       }
}

void spectrum() /*  create color spectrum palette */
{
    int i;
    if (v_mode == 0x65) /* 1024x768x16 mode only */
      {
        ext_palette( 0,  0,  0,  0); /* black */
        ext_palette( 1, 63,  0,  0); /* red */
        ext_palette( 2, 63, 21,  0);
        ext_palette( 3, 63, 42,  0);
        ext_palette( 4, 63, 63,  0); /* yellow */
        ext_palette( 5, 42, 63,  0);
        ext_palette( 6, 21, 63,  0);
        ext_palette( 7,  0, 63,  0); /* green */
        ext_palette( 8,  0, 63, 21);
        ext_palette( 9,  0, 63, 42);
        ext_palette(10,  0, 63, 63); /* cyan */
        ext_palette(11,  0, 42, 63);
        ext_palette(12,  0, 21, 63);
        ext_palette(13,  0,  0,  63); /* blue */
        ext_palette(14, 21,  0,  63);
        ext_palette(15, 42,  0,  63); /* magenta */
      }
    else
      {
        if (v_color == 255) /* all 256 color modes */
          {
            vga_palette(0,0,0,0);
            for (i=1; i<=64; i++) { vga_palette(i,63,i-1,0); }
            for (i=65; i<=128; i++) { vga_palette(i,128-i,63,0); }
            for (i=129; i<=192; i++) { vga_palette(i,0,63,i-129); }
            for (i=193; i<=255; i++) { vga_palette(i,0,255-i,63); }
          }
        else
          { /*  16 color EGA modes except 1024x768 */
            set_palette(ega_palette);
          }
      }
}
void ext_palette(i,r,g,b) /* write to external palette registers */
unsigned char i,r,g,b;
{
#define outp(p,v) dx = p; al = v; asm(dx,al,"     out     dx,al")
    reg$eax unsigned char al,v;
    reg$edx unsigned short dx,p;
    outp(0x3C8, i);
    outp(0x3C9, r);
    outp(0x3C9, g);
    outp(0x3C9, b);
    outp(0x3C8, i<<4);
    outp(0x3C9, r);
    outp(0x3C9, g);
    outp(0x3C9, b);
}

Listing 2

asm("    push  es");      /*  save segment register */
asm("    mov  ax, 034h"); /*  use the Phar Lap Local Descriptor Table (LDT) */
asm("    mov  es, ax");   /*  segment selector 034h to access real memory */
ebx = addr;               /*  real memory address desired */
cl = val;                 /*  byte value to poke */
asm(ebx, cl, "   mov   byte ptr es:[ebx], cl"); /*  poke it */
asm("    pop  es")        /*  restore segment register */

Listing 3

8-bit byte:          cl = val;
                  asm(ebx, cl, "   mov   byte ptr es:[ebx], cl");
16-bit word:         cx = val;
                  asm(ebx, cx, "   mov   word ptr es:[ebx], cx");
32-bit double word   ecx = val;
                  asm(ebx, ecx, "   mov   dword ptr es:[ebx], ecx");

Listing 4

      asm("    push   es");        /*  save es */
      asm("    mov    ax, 034h");  /*  use Phar Lap LDT to access real */
      asm("    mov    es, ax");
      dx = 0x3CE;                  /*  EGA graphics address register */
      ebx= 655360 + ((y*colx+x) >> 3); /*  memory position of pixel */
  asm(ebx, "    mov    cl, byte ptr es:[ebx]"); /*  load EGA registers */
      ax = color << 8;
asm(dx, ax, "   out    dx, ax");    /*  set color */
      ax = 0x0 F01;
asm(dx, ax, "   out    dx, ax");    /*  enable */
      ax = 0x0003; /* 0x00 = replace, 0x10 OR, 0x18 XOR, 0x08 AND */
asm(dx, ax, "   out    dx, ax");    /*  pixel write mode */
      ax = ((0x80 >> (x % 8)) << 8 ) + 8;
asm(dx, ax, "   out    dx, ax");    /*  bit mask (8 pixels/byte) */
  asm(ebx, "    mov    byte ptr es:[ebx],  255"); /*  write EGA registers */
      asm("    pop    es");        /*  restore es */

Listing 5

ch = (unsigned char)((y * colx + x) >> 15);    /*  find current page */
if (ch != old_page)                /*  only change page if different */
  {
    dx = ati_extreg;      /*  location of ATI card external register */
    asm("     cli     ");          /*  disable interrupts */
    asm("     mov     al,0b2h");   /*  page select */
  asm(dx,"    out     dx,al");      /*  ATI extended register */
    asm("     inc     dl");
    asm("     in      al,dx");
    asm("     mov     ah,al");
    asm("     and     ah,0e1h");    /*  page mask */
  asm(ch,"    or      ah,ch");      /*  ch = memory page desired */
    asm("     mov     al,0b2h");    /*  page select */
    asm("     dec     dl");
    asm("     out     dx, ax");
    asm("     sti     ");           /*  enable interrupts */
    old_page = ch;
  }
addr = 655360 + ((y * colx + x) % 65536);
poke(addr, color);          /*  write direct to real video memory */

Listing 6

ch = (char)(y >> 6);
if (ch != old_page) /* only change page if different */
  {
    dx = ati_extreg;
    asm("     cli    ");
    asm("     mov    al,0b2h");
  asm(dx,"    out    dx,al");
    asm("     inc    dl");
    asm("     in     al,dx");
    asm("     mov    ah,al");
    asm("     and    ah,0e1h");
  asm(ch,"    or     ah,ch");
    asm("     mov    al,0b2h");
    asm("     dec    dl");
    asm("     out    dx,ax" );
    asm("     sti    ");
    old_page = ch;
  }
addr = 655360 + ((y << 9) % 65536) + (x >> 1);
peek(addr, val); /* read existing color of pixel pair */
if (x % 2) val = color | (val & 0xF0); /* change left pixel */
else val = (color << 4) | (val & 0x0F); /* change right pixel */
poke(addr, val); /* write pixel pair */