Skip to content

Latest commit

 

History

History
1038 lines (930 loc) · 47.4 KB

chapter-44.md

File metadata and controls

1038 lines (930 loc) · 47.4 KB
title author date identifier publisher category chapter pages
Michael Abrash's Graphics Programming Black Book, Special Edition
Michael Abrash
1997-07-01
scheme text
ISBN
1576101746
The Coriolis Group
Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development
44
817-837

Chapter 44 -- Split Screens Save the Page Flipped Day

640x480 Page Flipped Animation in 64K...Almost

Almost doesn't count, they say—at least in horseshoes and maybe a few other things. This is especially true in digital circles, where if you need 12 MB of hard disk to install something and you only have 10 MB left (a situation that seems to be some sort of eternal law) you're stuck.

And that's only infuriating until you dredge up the gumption to go in there and free up some space. How would you feel if you were up against an "almost-but-not-quite" kind of a wall that couldn't be breached by freeing up something elsewhere? Suppose you were within a few KB of implementing a wonderful VGA animation scheme that provided lots of screen space, square pixels, smooth motion and more than adequate speed—but all the memory you have is all there is? What would you do?

Scream a little. Or throw something that won't break easily. Then you sit down and let your right brain do what it was designed to do. Sure enough, there's a way, and in this chapter I'll explain how a little VGA secret called page splitting can save the day for page flipped animation in 640x480 mode. But to do that, I have to lay a little groundwork first. Or maybe a lot of groundwork.

No horseshoes here.

A Plethora of Challenges

In its simplest terms, computer animation consists of rapidly redrawing similar images at slightly differing locations, so that the eye interprets the successive images as a single object in motion over time. The fact that the world is an analog realm and the images displayed on a computer screen consist of discrete pixels updated at a maximum rate of about 70 Hz is irrelevant; your eye can interpret both real-world images and pixel patterns on the screen as objects in motion, and that's that.

One of the key problems of computer animation is that it takes time to redraw a screen, time during which the bitmap controlling the screen is in an intermediate state, with, quite possibly, many objects erased and others half-drawn. Even when only briefly displayed, a partially-updated screen can cause flicker at best, and at worst can destroy the illusion of motion entirely.

Another problem of animation is that the screen must update often enough so that motion appears continuous. A moving object that moves just once every second, shifting by hundreds of pixels each time it does move, will appear to jump, not to move smoothly. Therefore, there are two overriding requirements for smooth animation: 1) the bitmap must be updated quickly (once per frame—60 to 70 Hz—is ideal, although 30 Hz will do fine), and, 2) the process of redrawing the screen must be invisible to the user; only the end result should ever be seen. Both of these requirements are met by the program presented in Listings 44.1 and 44.2.

A Page Flipping Animation Demonstration

The listings taken together form a sample animation program, in which a single object bounces endlessly off other objects, with instructions and a count of bounces displayed at the bottom of the screen. I'll discuss various aspects of Listings 44.1 and 44.2 during the balance of this article. The listings are too complex and involve too much VGA and animation knowledge for for me to discuss it all in exhaustive detail (and I've covered a lot of this stuff earlier in the book); instead, I'll cover the major elements, leaving it to you to explore the finer points—and, hope, to experiment with and expand on the code I'll provide.

LISTING 44.1 L44-1.C

/* Split screen VGA animation program. Performs page flipping in the
top portion of the screen while displaying non-page flipped
information in the split screen at the bottom of the screen.
Compiled with Borland C++ in C compilation mode. */

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

#define SCREEN-SEG         0xA000
#define SCREEN-PIXWIDTH    640   /* in pixels */
#define SCREEN-WIDTH       80    /* in bytes */
#define SPLIT-START-LINE   339
#define SPLIT-LINES        141
#define NONSPLIT-LINES     339
#define SPLIT-START-OFFSET 0
#define PAGE0-START-OFFSET (SPLIT-LINES*SCREEN-WIDTH)
#define PAGE1-START-OFFSET ((SPLIT-LINES+NONSPLIT-LINES)*SCREEN-WIDTH)
#define CRTC-INDEX   0x3D4 /* CRT Controller Index register */
#define CRTC-DATA    0x3D5 /* CRT Controller Data register */
#define OVERFLOW     0x07  /* index of CRTC reg holding bit 8 of the
                              line the split screen starts after */
#define MAX-SCAN     0x09  /* index of CRTC reg holding bit 9 of the
                              line the split screen starts after */
#define LINE-COMPARE 0x18  /* index of CRTC reg holding lower 8 bits
                              of line split screen starts after */
#define NUM-BUMPERS  (sizeof(Bumpers)/sizeof(bumper))
#define BOUNCER-COLOR 15
#define BACK-COLOR   1     /* playfield background color */

typedef struct {  /* one solid bumper to be bounced off of */
   int LeftX,TopY,RightX,BottomY;
   int Color;
} bumper;

typedef struct {     /* one bit pattern to be used for drawing */
   int WidthInBytes;
   int Height;
   unsigned char *BitPattern;
} image;

typedef struct {  /* one bouncing object to move around the screen */
   int LeftX,TopY;         /* location */
   int Width,Height;       /* size in pixels */
   int DirX,DirY;          /* motion vectors */
   int CurrentX[2],CurrentY[2]; /* current location in each page */
   int Color;              /* color in which to be drawn */
   image *Rotation0;       /* rotations for handling the 8 possible */
   image *Rotation1;       /* intrabyte start address at which the */
   image *Rotation2;       /* left edge can be */
   image *Rotation3;
   image *Rotation4;
   image *Rotation5;
   image *Rotation6;
   image *Rotation7;
} bouncer;

void main(void);
void DrawBumperList(bumper *, int, unsigned int);
void DrawSplitScreen(void);
void EnableSplitScreen(void);
void MoveBouncer(bouncer *, bumper *, int);
extern void DrawRect(int,int,int,int,int,unsigned int,unsigned int);
extern void ShowPage(unsigned int);
extern void DrawImage(int,int,image **,int,unsigned int,unsigned int);
extern void ShowBounceCount(void);
extern void TextUp(char *,int,int,unsigned int,unsigned int);
extern void SetBIOS8x8Font(void);

/* All bumpers in the playfield */
bumper Bumpers[] = {
   {0,0,19,339,2}, {0,0,639,19,2}, {620,0,639,339,2},
   {0,320,639,339,2}, {60,48,79,67,12}, {60,108,79,127,12},
   {60,168,79,187,12}, {60,228,79,247,12}, {120,68,131,131,13},
   {120,188,131,271,13}, {240,128,259,147,14}, {240,192,259,211,14},
   {208,160,227,179,14}, {272,160,291,179,14}, {228,272,231,319,11},
   {192,52,211,55,11}, {302,80,351,99,12}, {320,260,379,267,13},
   {380,120,387,267,13}, {420,60,579,63,11}, {428,110,571,113,11},
   {420,160,579,163,11}, {428,210,571,213,11}, {420,260,579,263,11} };

/* Image for bouncing object when left edge is aligned with bit 7 */
unsigned char -BouncerRotation0[] = {
   0xFF,0x0F,0xF0, 0xFE,0x07,0xF0, 0xFC,0x03,0xF0, 0xFC,0x03,0xF0,
   0xFE,0x07,0xF0, 0xFF,0xFF,0xF0, 0xCF,0xFF,0x30, 0x87,0xFE,0x10,
   0x07,0x0E,0x00, 0x07,0x0E,0x00, 0x07,0x0E,0x00, 0x07,0x0E,0x00,
   0x87,0xFE,0x10, 0xCF,0xFF,0x30, 0xFF,0xFF,0xF0, 0xFE,0x07,0xF0,
   0xFC,0x03,0xF0, 0xFC,0x03,0xF0, 0xFE,0x07,0xF0, 0xFF,0x0F,0xF0};
image BouncerRotation0 = {3, 20, -BouncerRotation0};

/* Image for bouncing object when left edge is aligned with bit 3 */
unsigned char -BouncerRotation4[] = {
   0x0F,0xF0,0xFF, 0x0F,0xE0,0x7F, 0x0F,0xC0,0x3F, 0x0F,0xC0,0x3F,
   0x0F,0xE0,0x7F, 0x0F,0xFF,0xFF, 0x0C,0xFF,0xF3, 0x08,0x7F,0xE1,
   0x00,0x70,0xE0, 0x00,0x70,0xE0, 0x00,0x70,0xE0, 0x00,0x70,0xE0,
   0x08,0x7F,0xE1, 0x0C,0xFF,0xF3, 0x0F,0xFF,0xFF, 0x0F,0xE0,0x7F,
   0x0F,0xC0,0x3F, 0x0F,0xC0,0x3F, 0x0F,0xE0,0x7F, 0x0F,0xF0,0xFF};
image BouncerRotation4 = {3, 20, -BouncerRotation4};

/* Initial settings for bouncing object. Only 2 rotations are needed
   because the object moves 4 pixels horizontally at a time */
bouncer Bouncer = {156,60,20,20,4,4,156,156,60,60,BOUNCER-COLOR,
   &BouncerRotation0,NULL,NULL,NULL,&BouncerRotation4,NULL,NULL,NULL};
unsigned int PageStartOffsets[2] =
   {PAGE0-START-OFFSET,PAGE1-START-OFFSET};
unsigned int BounceCount;

void main() {
   int DisplayedPage, NonDisplayedPage, Done, i;
   union REGS regset;

   regset.x.ax = 0x0012; /* set display to 640x480 16-color mode */
   int86(0x10, &regset, &regset);
   SetBIOS8x8Font();    /* set the pointer to the BIOS 8x8 font */
   EnableSplitScreen(); /* turn on the split screen */

   /* Display page 0 above the split screen */
   ShowPage(PageStartOffsets[DisplayedPage = 0]);

   /* Clear both pages to background and draw bumpers in each page */
   for (i=0; i<2; i++) {
      DrawRect(0,0,SCREEN-PIXWIDTH-1,NONSPLIT-LINES-1,BACK-COLOR,
            PageStartOffsets[i],SCREEN-SEG);
      DrawBumperList(Bumpers,NUM-BUMPERS,PageStartOffsets[i]);
   }

   DrawSplitScreen();   /* draw the static split screen info */
   BounceCount = 0;
   ShowBounceCount();   /* put up the initial zero count */

   /* Draw the bouncing object at its initial location */
   DrawImage(Bouncer.LeftX,Bouncer.TopY,&Bouncer.Rotation0,
         Bouncer.Color,PageStartOffsets[DisplayedPage],SCREEN-SEG);

   /* Move the object, draw it in the nondisplayed page, and flip the
      page until Esc is pressed */
   Done = 0;
   do {
      NonDisplayedPage = DisplayedPage ^ 1;
      /* Erase at current location in the nondisplayed page */
      DrawRect(Bouncer.CurrentX[NonDisplayedPage],
            Bouncer.CurrentY[NonDisplayedPage],
            Bouncer.CurrentX[NonDisplayedPage]+Bouncer.Width-1,
            Bouncer.CurrentY[NonDisplayedPage]+Bouncer.Height-1,
            BACK-COLOR,PageStartOffsets[NonDisplayedPage],SCREEN-SEG);
      /* Move the bouncer */
      MoveBouncer(&Bouncer, Bumpers, NUM-BUMPERS);
      /* Draw at the new location in the nondisplayed page */
      DrawImage(Bouncer.LeftX,Bouncer.TopY,&Bouncer.Rotation0,
            Bouncer.Color,PageStartOffsets[NonDisplayedPage],
            SCREEN-SEG);
      /* Remember where the bouncer is in the nondisplayed page */
      Bouncer.CurrentX[NonDisplayedPage] = Bouncer.LeftX;
      Bouncer.CurrentY[NonDisplayedPage] = Bouncer.TopY;
      /* Flip to the page we just drew into */
      ShowPage(PageStartOffsets[DisplayedPage = NonDisplayedPage]);
      /* Respond to any keystroke */
      if (kbhit()) {
         switch (getch()) {
            case 0x1B:           /* Esc to end */
               Done = 1; break;
            case 0:              /* branch on the extended code */
               switch (getch()) {
                  case 0x48:  /* nudge up */
                     Bouncer.DirY = -abs(Bouncer.DirY); break;
                  case 0x4B:  /* nudge left */
                     Bouncer.DirX = -abs(Bouncer.DirX); break;
                  case 0x4D:  /* nudge right */
                     Bouncer.DirX = abs(Bouncer.DirX); break;
                  case 0x50:  /* nudge down */
                     Bouncer.DirY = abs(Bouncer.DirY); break;
               }
               break;
            default:
               break;
         }
      }
   } while (!Done);

   /* Restore text mode and done */
   regset.x.ax = 0x0003;
   int86(0x10, &regset, &regset);
}

/* Draws the specified list of bumpers into the specified page */
void DrawBumperList(bumper * Bumpers, int NumBumpers,
      unsigned int PageStartOffset)
{
   int i;

   for (i=0; i<NumBumpers; i++,Bumpers++) {
      DrawRect(Bumpers->LeftX,Bumpers->TopY,Bumpers->RightX,
            Bumpers->BottomY,Bumpers->Color,PageStartOffset,
            SCREEN-SEG);
   }
}

/* Displays the current bounce count */
void ShowBounceCount() {
   char CountASCII[7];

   itoa(BounceCount,CountASCII,10); /* convert the count to ASCII*/
   TextUp(CountASCII,344,64,SPLIT-START-OFFSET,SCREEN-SEG);
}

/* Frames the split screen and fills it with various text */
void DrawSplitScreen() {
   DrawRect(0,0,SCREEN-PIXWIDTH-1,SPLIT-LINES-1,0,SPLIT-START-OFFSET,
         SCREEN-SEG);
   DrawRect(0,1,SCREEN-PIXWIDTH-1,4,15,SPLIT-START-OFFSET,
         SCREEN-SEG);
   DrawRect(0,SPLIT-LINES-4,SCREEN-PIXWIDTH-1,SPLIT-LINES-1,15,
         SPLIT-START-OFFSET,SCREEN-SEG);
   DrawRect(0,1,3,SPLIT-LINES-1,15,SPLIT-START-OFFSET,SCREEN-SEG);
   DrawRect(SCREEN-PIXWIDTH-4,1,SCREEN-PIXWIDTH-1,SPLIT-LINES-1,15,
         SPLIT-START-OFFSET,SCREEN-SEG);
   TextUp("This is the split screen area...",8,8,SPLIT-START-OFFSET,
         SCREEN-SEG);
   TextUp("Bounces: ",272,64,SPLIT-START-OFFSET,SCREEN-SEG);
   TextUp("\033: nudge left",520,78,SPLIT-START-OFFSET,SCREEN-SEG);
   TextUp("\032: nudge right",520,90,SPLIT-START-OFFSET,SCREEN-SEG);
   TextUp("\031: nudge down",520,102,SPLIT-START-OFFSET,SCREEN-SEG);
   TextUp("\030: nudge up",520,114,SPLIT-START-OFFSET,SCREEN-SEG);
   TextUp("Esc to end",520,126,SPLIT-START-OFFSET,SCREEN-SEG);
}

/* Turn on the split screen at the desired line (minus 1 because the
   split screen starts *after* the line specified by the LINE-COMPARE
   register) (bit 8 of the split screen start line is stored in the
   Overflow register, and bit 9 is in the Maximum Scan Line reg) */
void EnableSplitScreen() {
   outp(CRTC-INDEX, LINE-COMPARE);
   outp(CRTC-DATA, (SPLIT-START-LINE - 1) & 0xFF);
   outp(CRTC-INDEX, OVERFLOW);
   outp(CRTC-DATA, (((((SPLIT-START-LINE - 1) & 0x100) >> 8) << 4) |
         (inp(CRTC-DATA) & ~0x10)));
   outp(CRTC-INDEX, MAX-SCAN);
   outp(CRTC-DATA, (((((SPLIT-START-LINE - 1) & 0x200) >> 9) << 6) |
         (inp(CRTC-DATA) & ~0x40)));
}

/* Moves the bouncer, bouncing if bumpers are hit */
void MoveBouncer(bouncer *Bouncer, bumper *BumperPtr, int NumBumpers) {
   int NewLeftX, NewTopY, NewRightX, NewBottomY, i;

   /* Move to new location, bouncing if necessary */
   NewLeftX = Bouncer->LeftX + Bouncer->DirX;   /* new coords */
   NewTopY = Bouncer->TopY + Bouncer->DirY;
   NewRightX = NewLeftX + Bouncer->Width - 1;
   NewBottomY = NewTopY + Bouncer->Height - 1;
   /* Compare the new location to all bumpers, checking for bounce */
   for (i=0; i<NumBumpers; i++,BumperPtr++) {
      /* If moving puts the bouncer inside this bumper, bounce */
      if (  (NewLeftX <= BumperPtr->RightX) &&
            (NewRightX >= BumperPtr->LeftX) &&
            (NewTopY <= BumperPtr->BottomY) &&
            (NewBottomY >= BumperPtr->TopY) ) {
         /* The bouncer has tried to move into this bumper; figure
            out which edge(s) it crossed, and bounce accordingly */
         if (((Bouncer->LeftX > BumperPtr->RightX) &&
               (NewLeftX <= BumperPtr->RightX)) ||
               (((Bouncer->LeftX + Bouncer->Width - 1) <
               BumperPtr->LeftX) &&
               (NewRightX >= BumperPtr->LeftX))) {
            Bouncer->DirX = -Bouncer->DirX;  /* bounce horizontally */
            NewLeftX = Bouncer->LeftX + Bouncer->DirX;
         }
         if (((Bouncer->TopY > BumperPtr->BottomY) &&
               (NewTopY <= BumperPtr->BottomY)) ||
               (((Bouncer->TopY + Bouncer->Height - 1) <
               BumperPtr->TopY) &&
               (NewBottomY >= BumperPtr->TopY))) {
            Bouncer->DirY = -Bouncer->DirY; /* bounce vertically */
            NewTopY = Bouncer->TopY + Bouncer->DirY;
         }
         /* Update the bounce count display; turn over at 10000 */
         if (++BounceCount >= 10000) {
            TextUp("0    ",344,64,SPLIT-START-OFFSET,SCREEN-SEG);
            BounceCount = 0;
         } else {
            ShowBounceCount();
         }
      }
   }
   Bouncer->LeftX = NewLeftX; /* set the final new coordinates */
   Bouncer->TopY = NewTopY;
}

LISTING 44.2 L44-2.ASM

; Low-level animation routines.
; Tested with TASM

SCREEN-WIDTH       equ     80      ;screen width in bytes
INPUT-STATUS-1     equ     03dah   ;Input Status 1 register
CRTC-INDEX         equ     03d4h   ;CRT Controller Index reg
START-ADDRESS-HIGH equ     0ch     ;bitmap start address high byte
START-ADDRESS-LOW  equ     0dh     ;bitmap start address low byte
GC-INDEX           equ     03ceh   ;Graphics Controller Index reg
SET-RESET          equ     0       ;GC index of Set/Reset reg
G-MODE             equ     5       ;GC index of Mode register

        .model  small
        .data
BIOS8x8Ptr dd   ?       ;points to BIOS 8x8 font
; Tables used to look up left and right clip masks.
LeftMask d    0ffh, 07fh, 03fh, 01fh, 00fh, 007h, 003h, 001h
RightMask d   080h, 0c0h, 0e0h, 0f0h, 0f8h, 0fch, 0feh, 0ffh

        .code
; Draws the specified filled rectangle in the specified color.
; Assumes the display is in mode 12h. Does not clip and assumes
; rectangle coordinates are valid.
;
; C near-callable as: void DrawRect(int LeftX, int TopY, int RightX,
;       int BottomY, int Color, unsigned int ScrnOffset,
;       unsigned int ScrnSegment);

DrawRectParms   struc
        dw      2 dup (?) ;pushed BP and return address
LeftX   dw      ?               ;X coordinate of left side of rectangle
TopY    dw      ?               ;Y coordinate of top side of rectangle
RightX  dw      ?               ;X coordinate of right side of rectangle
BottomY dw      ?               ;Y coordinate of bottom side of rectangle
Color   dw      ?               ;color in which to draw rectangle (only the
                                ; lower 4 bits matter)
ScrnOffset  dw  ?               ;offset of base of bitmap in which to draw
ScrnSegment dw  ?               ;segment of base of bitmap in which to draw
DrawRectParms   ends

        public  -DrawRect
-DrawRect       proc    near
        push    bp              ;preserve caller's stack frame
        mov     bp,sp           ;point to local stack frame
        push    si              ;preserve caller's register variables
        push    di

        cld
        mov     dx,GC-INDEX
        mov     al,SET-RESET
        mov     ah,byte ptr Color[bp]
        out     dx,ax           ;set the color in which to draw
        mov     ax,G-MODE + (0300h)
        out     dx,ax           ;set to write mode 3
        les     di,dword ptr ScrnOffset[bp] ;point to bitmap start
        mov     ax,SCREEN-WIDTH
        mul     TopY[bp]         ;point to the start of the top scan
        add     di,ax            ; line to fill
        mov     ax,LeftX[bp]
        mov     bx,ax
        shr     ax,1             ;/8 = byte offset from left of screen
        shr     ax,1
        shr     ax,1
        add     di,ax            ;point to the upper-left corner of fill area
        and     bx,7             ;isolate intrapixel address
        mov     dl,LeftMask[bx]  ;set the left-edge clip mask
        mov     bx,RightX[bp]
        mov     si,bx
        and     bx,7             ;isolate intrapixel address of right edge
        mov     dh,RightMask[bx] ;set the right-edge clip mask
        mov     bx,LeftX[bp]
        and     bx,NOT 7         ;intrapixel address of left edge
        sub     si,bx
        shr     si,1
        shr     si,1
        shr     si,1             ;# of bytes across spanned by rectangle - 1
        jnz     MasksSet         ;if there's only one byte across,
        and     dl,dh            ; combine the masks
MasksSet:
        mov     bx,BottomY[bp]
        sub     bx,TopY[bp]        ;# of scan lines to fill - 1
FillLoop:
        push    di               ;remember line start offset
        mov     al,dl            ;left edge clip mask
        xchg    es:[di],al       ;draw the left edge
        inc     di               ;point to the next byte
        mov     cx,si            ;# of bytes left to do
        dec     cx               ;# of bytes left to do - 1
        js      LineDone         ;that's it if there's only 1 byte across
        jz      DrawRightEdge    ;no middle bytes if only 2 bytes across
        mov     al,0ffh          ;non-edge bytes are solid
        rep     stos             ;draw the solid bytes across the middle
DrawRightEdge:
        mov     al,dh            ;right-edge clip mask
        xchg    es:[di],al       ;draw the right edge
LineDone:
        pop     di               ;retrieve line start offset
        add     di,SCREEN-WIDTH  ;point to the next line
        dec     bx               ;count off scan lines
        jns     FillLoop

        pop     di               ;restore caller's register variables
        pop     si
        pop     bp               ;restore caller's stack frame
        ret
-DrawRect       endp

; Shows the page at the specified offset in the bitmap. Page is
; displayed when this routine returns.
;
; C near-callable as: void ShowPage(unsigned int StartOffset);

ShowPageParms   struc
        dw      2 dup (?)         ;pushed BP and return address
StartOffset dw  ?           ;offset in bitmap of page to display
ShowPageParms   ends

        public  -ShowPage
-ShowPage       proc    near
        push    bp                ;preserve caller's stack frame
        mov     bp,sp             ;point to local stack frame
; Wait for display enable to be active (status is active low), to be
; sure both halves of the start address will take in the same frame.
        mov     bl,START-ADDRESS-LOW        ;preload for fastest
        mov     bh,byte ptr StartOffset[bp] ; flipping once display
        mov     cl,START-ADDRESS-HIGH       ; enable is detected
        mov     ch,byte ptr StartOffset+1[bp]
        mov     dx,INPUT-STATUS-1
WaitDE:
        in      al,dx
        test    al,01h
        jnz     WaitDE            ;display enable is active low (0 = active)
; Set the start offset in display memory of the page to display.
        mov     dx,CRTC-INDEX
        mov     ax,bx
        out     dx,ax             ;start address low
        mov     ax,cx
        out     dx,ax             ;start address high
; Now wait for vertical sync, so the other page will be invisible when
; we start drawing to it.
        mov     dx,INPUT-STATUS-1
WaitVS:
        in      al,dx
        test    al,08h
        jz      WaitVS            ;vertical sync is active high (1 = active)
        pop     bp                ;restore caller's stack frame
        ret
-ShowPage       endp

; Displays the specified image at the specified location in the
; specified bitmap, in the desired color.
;
; C near-callable as: void DrawImage(int LeftX, int TopY,
;       image **RotationTable, int Color, unsigned int ScrnOffset,
;       unsigned int ScrnSegment);

DrawImageParms  struc
        dw      2 dup (?) ;pushed BP and return address
DILeftX       dw   ?            ;X coordinate of left side of image
DITopY        dw   ?            ;Y coordinate of top side of image
RotationTable dw   ?            ;pointer to table of pointers to image
                                ; rotations
DIColor       dw   ?            ;color in which to draw image (only the
                                ; lower 4 bits matter)
DIScrnOffset  dw   ?            ;offset of base of bitmap in which to draw
DIScrnSegment dw   ?            ;segment of base of bitmap in which to draw
DrawImageParms  ends

image struc
WidthInBytes    dw      ?
Height          dw      ?
BitPattern      dw      ?
image ends

        public  -DrawImage
-DrawImage      proc    near
        push    bp              ;preserve caller's stack frame
        mov     bp,sp           ;point to local stack frame
        push    si              ;preserve caller's register variables
        push    di

        cld
        mov     dx,GC-INDEX
        mov     al,SET-RESET
        mov     ah,byte ptr DIColor[bp]
        out     dx,ax           ;set the color in which to draw
        mov     ax,G-MODE + (0300h)
        out     dx,ax           ;set to write mode 3
        les     di,dword ptr DIScrnOffset[bp] ;point to bitmap start
        mov     ax,SCREEN-WIDTH
        mul     DITopY[bp]      ;point to the start of the top scan
        add     di,ax           ; line on which to draw
        mov     ax,DILeftX[bp]
        mov     bx,ax
        shr     ax,1            ;/8 = byte offset from left of screen
        shr     ax,1
        shr     ax,1
        add     di,ax           ;point to the upper-left corner of draw area
        and     bx,7            ;isolate intrapixel address
        shl     bx,1            ;*2 for word look-up
        add     bx,RotationTable[bp] ;point to the image structure for
        mov     bx,[bx]              ; the intrabyte rotation
        mov     dx,[bx].WidthInBytes ;image width
        mov     si,[bx].BitPattern   ;pointer to image pattern bytes
        mov     bx,[bx].Height       ;image height
DrawImageLoop:
        push    di                   ;remember line start offset
        mov     cx,dx                ;# of bytes across
DrawImageLineLoop:
        lods                         ;get the next image byte
        xchg    es:[di],al           ;draw the next image byte
        inc     di                   ;point to the following screen byte
        loop    DrawImageLineLoop
        pop     di                   ;retrieve line start offset
        add     di,SCREEN-WIDTH      ;point to the next line
        dec     bx                   ;count off scan lines
        jnz     DrawImageLoop

        pop     di                   ;restore caller's register variables
        pop     si
        pop     bp                   ;restore caller's stack frame
        ret
-DrawImage      endp

; Draws a 0-terminated text string at the specified location in the
; specified bitmap in white, using the 8x8 BIOS font. Must be at an X
; coordinate that's a multiple of 8.
;
; C near-callable as: void TextUp(char *Text, int LeftX, int TopY,
;       unsigned int ScrnOffset, unsigned int ScrnSegment);

TextUpParms     struc
                 dw    2 dup (?) ;pushed BP and return address
Text             dw    ?            ;pointer to text to draw
TULeftX          dw    ?            ;X coordinate of left side of rectangle
                                    ; (must be a multiple of 8)
TUTopY           dw    ?            ;Y coordinate of top side of rectangle
TUScrnOffset     dw    ?            ;offset of base of bitmap in which to draw
TUScrnSegment    dw    ?            ;segment of base of bitmap in which to draw
TextUpParms     ends

        public  -TextUp
-TextUp proc    near
        push    bp                  ;preserve caller's stack frame
        mov     bp,sp               ;point to local stack frame
        push    si                  ;preserve caller's register variables
        push    di

        cld
        mov     dx,GC-INDEX
        mov     ax,G-MODE + (0000h)
        out     dx,ax   ;set to write mode 0
        les     di,dword ptr TUScrnOffset[bp] ;point to bitmap start
        mov     ax,SCREEN-WIDTH
        mul     TUTopY[bp]           ;point to the start of the top scan
        add     di,ax                ; line the text starts on
        mov     ax,TULeftX[bp]
        mov     bx,ax
        shr     ax,1                 ;/8 = byte offset from left of screen
        shr     ax,1
        shr     ax,1
        add     di,ax                ;point to the upper-left corner of first char
        mov     si,Text[bp]          ;point to text to draw
TextUpLoop:
        lods                         ;get the next character to draw
        and     al,al
        jz      TextUpDone           ;done if null byte
        push    si                   ;preserve text string pointer
        push    di                   ;preserve character's screen offset
        push    ds                   ;preserve default data segment
        call    CharUp               ;draw this character
        pop     ds                   ;restore default data segment
        pop     di                   ;retrieve character's screen offset
        pop     si                   ;retrieve text string pointer
        inc     di                   ;point to next character's start location
        jmp     TextUpLoop

TextUpDone:
        pop     di                   ;restore caller's register variables
        pop     si
        pop     bp                   ;restore caller's stack frame
        ret

CharUp:                              ;draws the character in AL at ES:DI
        lds     si,[BIOS8x8Ptr]      ;point to the 8x8 font start
        mov     bl,al
        sub     bh,bh
        shl     bx,1
        shl     bx,1
        shl     bx,1                 ;*8 to look up character offset in font
        add     si,bx                ;point DS:Sito character data in font
        mov     cx,8                 ;characters are 8 high
CharUpLoop:
        movs                         ;copy the next character pattern byte
        add     di,SCREEN-WIDTH-1    ;point to the next dest byte
        loop    CharUpLoop
        ret
-TextUp endp

; Sets the pointer to the BIOS 8x8 font.
;
; C near-callable as: extern void SetBIOS8x8Font(void);

        public  -SetBIOS8x8Font
-SetBIOS8x8Font proc    near
        push    bp                  ;preserve caller's stack frame
        push    si                  ;preserve caller's register variables
        push    di                  ; and data segment (don't assume BIOS
        push    ds                  ; preserves anything)
        mov     ah,11h              ;BIOS character generator function
        mov     al,30h              ;BIOS information subfunction
        mov     bh,3                ;request 8x8 font pointer
        int     10h                 ;invoke BIOS video services
        mov     word ptr [BIOS8x8Ptr],bp  ;store the pointer
        mov     word ptr [BIOS8x8Ptr+2],es
        pop     ds
        pop     di                  ;restore caller's register variables
        pop     si
        pop     bp                  ;restore caller's stack frame
        ret
-SetBIOS8x8Font endp
        end

Listing 44.1 is written in C. It could equally well have been written in assembly language, and would then have been somewhat faster. However, wanted to make the point (as I've made again and again) that assembly language, and, indeed, optimization in general, is needed only in the most critical portions of any program, and then only when the program would otherwise be too slow. Only in a highly performance-sensitive situation would the performance boost resulting from converting Listing 44.1 to assembly justify the time spent in coding and the bugs that would likely creep in—and the sample program already updates the screen at the maximum possible rate of once per frame even on a 1985-vintage 8-MHz AT. In this case, faster performance would result only in a longer wait for the page to flip.

Write Mode 3

It's possible to update the bitmap very efficiently on the VGA, because the VGA can draw up to 8 pixels at once, and because the VGA provides a number of hardware features to speed up drawing. This article makes considerable use of one particularly unusual hardware feature, write mode 3. We discussed write mode 3 back in Chapter 26, but we've covered a lot of ground since then—so I'm going to run through a quick refresher on write mode 3.

Some background: In the standard VGA's high-resolution mode, mode 12H (640x480 with 16 colors, the mode in which this chapter's sample program runs), each byte of display memory controls 8 adjacent pixels on the screen. (The color of each pixel is, in turn, controlled by 4 bits spread across the four VGA memory planes, but we need not concern ourselves with that here.) Now, there will often be times when we want to change some but not all of the pixels controlled by a particular byte of display memory. This is not easily done, for there is no way to write half a byte, or two bits, or such to memory; it's the whole byte or none of it at all.

You might think that using AND and OR to manipulate individual bits could solve the problem. Alas, not so. ANDing and ORing would work if the VGA had only one plane of memory (like the original monochrome Hercules Graphics Adapter) but the VGA has four planes, and ANDing and ORing would work only if we selected and manipulated each plane separately, a process that would be hideously slow. No, with the VGA you must use the hardware assist features, or you might as well forget about real-time screen updates altogether. Write mode 3 will do the trick for our present needs.

Write mode 3 is useful when you want to set some but not all of the pixels in a single byte of display memory to the same color. That is, if you want to draw a number of pixels within a byte in a single color, write mode 3 is a good way to do it.

Write mode 3 works like this. First, set the Graphics Controller Mode register to write mode 3. (Look at Listing 44.2 for code that does everything described here.) Next, set the Set/Reset register to the color with which you wish to draw, in the range 0-15. (It is not necessary to explicitly enable set/reset via the Enable Set/Reset register; write mode 3 does that automatically.) Then, to draw individual pixels within a single byte, simply read display memory, and then write a byte to display memory with 1-bits where you want the color to be drawn and 0-bits where you want the current bitmap contents to be preserved. (Note well that the data actually read by the CPU doesn't matter; the read operation latches all four planes' data, as described way back in Chapter 24.) So, for example, if write mode 3 is enabled and the Set/Reset register is set to 1 (blue), then the following sequence of operations:

mov   dx,0a000h
mov   es,dx
mov   al,es:[0]
mov   byte ptr es:[0],0f0h

will change the first 4 pixels on the screen (the left nibble of the byte at offset 0 in display memory) to blue, and will leave the next 4 pixels (the right nibble of the byte at offset 0) unchanged.

Using one MOV to read from display memory and another to write to display memory is not particularly efficient on some processors. In Listing 44.2, I instead use XCHG, which reads and then writes a memory location in a single operation, as in:

mov    dx,0a000h
mov    es,dx
mov    al,0f0h
xchg   es:[0],al

Again, the actual value that's read is irrelevant. In general, the XCHG approach is more compact than two MOVs, and is faster on 386 and earlier processors, but slower on 486s and Pentiums.

If all pixels in a byte of display memory are to be drawn in a single color, it's not necessary to read before writing, because none of the information in display memory at that byte needs to be preserved; a simple write of 0FFH (to draw all bits) will set all 8 pixels to the set/reset color:

mov   dx,0a000h
mov   es,dx
mov   byte ptr es:[di],0ffh

If you're familiar with VGA programming, you're no doubt aware that everything that can be done with write mode 3 can also be accomplished in write mode 0 or write mode 2 by using the Bit Mask register. However, setting the Bit Mask register requires at least one OUT per byte written, in addition to the read and write of display memory, and OUTs are often slower than display memory accesses, especially on 386s and 486s. One of the great virtues of write mode 3 is that it requires virtually no OUTs and is therefore substantially faster for masking than the other write modes.

In short, write mode 3 is a good choice for single-color drawing that modifies individual pixels within display memory bytes. Not coincidentally, the sample application draws only single-color objects within the animation area; this allows write mode 3 to be used for all drawing, in keeping with our desire for speedy screen updates.

Drawing Text

We'll need text in the sample application; is that also a good use for write mode 3? Sometimes it is, but not in this particular case.

Each character in a font is represented by a pattern of bits, with 1-bits representing character pixels and 0-bits representing background pixels. Since we'll be using the 8x8 font stored in the BIOS ROM (a pointer to which can be obtained by calling a BIOS service, as illustrated by Listing 44.2), each character is exactly 8 bits, or 1 byte wide. We'll further insist that characters be placed on byte boundaries (that is, with their left edges only at pixels with X coordinates that are multiples of 8); this means that the character bytes in the font are automatically aligned with display memory, and no rotation or clipping of characters is needed. Finally, we'll draw all text in white.

Given the above assumptions, drawing text is easy; we simply copy each byte of each character to the appropriate location in display memory, and voila, we're done. Text copying is done in write mode 0, in which the byte written to display memory is copied to all four planes at once; hence, 1-bits turn into white (color value 0FH, with 1-bits in all four planes), and 0-bits turn into black (color value 0). This is faster than using write mode 3 because write mode 3 requires a read/write of display memory (or at least preloading the latches with the background color), while the write mode 0 approach requires only a write to display memory.

Is write mode 0 always the best way to do text? Not at all. The write mode 0 approach described above draws both foreground and background pixels within the character box, forcing the background pixels to black at the same time that it forces the foreground pixels to white. If you want to draw transparent text (that is, draw only the character pixels, not the surrounding background box), write mode 3 is ideal. Also, matters get far more complicated if characters that aren't 8 pixels wide are drawn, or if characters are drawn starting at arbitrary pixel locations, without the multiple-of-8 column restriction, so that rotation and masking are required. Lastly, the Map Mask register can be used to draw text in colors other than white—but only if the background is black. Otherwise, the data remaining in the planes protected by the Map Mask will remain and can interfere with the colors of the text being drawn.

I'm not going to delve any deeper into the considerable issues of drawing VGA text; I just want to sensitize you to the existence of approaches other than the ones used in Listings 44.1 and 44.2. On the VGA, the rule is: If there's something you want to do, there probably are 10 ways to do it, each with unique strengths and weaknesses. Your mission, should you decide to accept it, is to figure out which one is best for your particular application.

Page Flipping

Now that we know how to update the screen reasonably quickly, it's time to get on to the fun stuff. Page flipping answers the second requirement for animation, by keeping bitmap changes off the screen until they're complete. In other words, page flipping guarantees that partially updated bitmaps are never seen.

How is it possible to update a bitmap without seeing the changes as they're made? Easy—with page flipping, there are two bitmaps; the program shows you one bitmap while it updates the other. Conceptually, it's that simple. In practice, unfortunately, it's not so simple, because of the design of the VGA. To understand why that is, we must look at how the VGA turns bytes in display memory into pixels on the screen.

The VGA bitmap is a linear 64 K block of memory. (True, most adapters nowadays are SuperVGAs with more than 256 K of display memory, but every make of SuperVGA has its own way of letting you access that extra memory, so going beyond standard VGA is a daunting and difficult task. Also, it's hard to manipulate the large frame buffers of SuperVGA modes fast enough for real-time animation.) Normally, the VGA picks up the first byte of memory (the byte at offset 0) and displays the corresponding 8 pixels on the screen, then picks up the byte at offset 1 and displays the next 8 pixels, and so on to the end of the screen. However, the offset of the first byte of display memory picked up during each frame is not fixed at 0, but is rather programmable by way of the Start Address High and Low registers, which together store the 16-bit offset in display memory at which the bitmap to be displayed during the next frame starts. So, for example, in mode 10H (640x350, 16 colors), a large enough bitmap to store a complete screen of information can be stored at display memory offsets 0 through 27,999, and another full bitmap could be stored at offsets 28,000 through 55,999, as shown in Figure 44.1. (I'm discussing 640x350 mode at the moment for good reason; we'll get to 640x480 shortly.) When the Start Address registers are set to 0, the first bitmap (or page) is displayed; when they are set to 28,000, the second bitmap is displayed. Page flipped animation can be performed by displaying page 0 and drawing to page 1, then setting the start address to page 1 to display that page and drawing to page 0, and so on ad infinitum.

Figure 44.1  Memory allocation for mode 10h page flipping.

Knowing When to Flip

There's a hitch, though, and that hitch is knowing exactly when it is that the page has flipped. The page doesn't flip the instant that you set the Start Address registers. The VGA loads the starting offset from the Start Address registers once before starting each frame, then pays those registers no nevermind until the next frame comes around. This means that you can set the Start Address registers whenever you want—but the page actually being displayed doesn't change until after the VGA loads that new offset in preparation for the next frame.

The potential problem should be obvious. Suppose that page 1 is being displayed, and you're updating page 0. You finish drawing to page 0, set the Start Address registers to 0 to switch to displaying page 0, and start updating page 1, which is no longer displayed. Or is it? If the VGA was in the middle of the current frame, displaying page 1, when you set the Start Address registers, then page 1 is going to be displayed for the rest of the frame, no matter what you do with the Start Address registers. If you start updating page 1 right away, any changes you make may well show up on the screen, because page 0 hasn't yet flipped to being displayed in place of page 1—and that defeats the whole purpose of page flipping.

To avoid this problem, it is mandatory that you wait until you're sure the page has flipped. The Start Address registers are, according to my tests, loaded at the start of the Vertical Sync signal, although that may not be the case with all VGA clones. The Vertical Sync status is provided as bit 3 of the Input Status 1 register, so it would seem that all you need to do to flip a page is set the new Start Address registers, wait for the start of the Vertical Sync pulse that indicates that the page has flipped, and be on your merry way.

Almost—but not quite. (Do I hear teeth gnashing in the background?) The problem is this: Suppose that, by coincidence, you set one of the Start Address registers just before the start of Vertical Sync, and the other right after the start of Vertical Sync. Why, then, for one frame the Start Address High value for one page would be mixed with the Start Address Low value for the other page, and, depending on the start address values, the whole screen could appear to shift any number of pixels for a single, horrible frame. This must never happen! The solution is to set the Start Address registers when you're certain Vertical Sync is not about to start. The easiest way to know that is to check for the Display Enable status (bit 0 of the Input Status 1 register) being active; that means that bitmap-controlled pixels are being scanned onto the screen, and, since Vertical Sync happens in the middle of the vertical non-display portion of the frame, Vertical Sync can never be anywhere nearby if Display Enable is active. (Note that one good alternative is to set up both pages with a start address that's a multiple of 256, and just change the Start Address High register and wait for Vertical Sync, with no Display Enable wait required.)

So, to flip pages, you must complete all drawing to the non-displayed page, wait for Display Enable to be active, set the new start address, and wait for Vertical Sync to be active. At that point, you can be fully confident that the page that you just flipped off the screen is not displayed and can safely (invisibly) be updated. A side benefit of page flipping is that your program will automatically have a constant time base, with the rate at which new screens are drawn synchronized to the frame rate of the display (typically 60 or 70 Hz). However, complex updates may take more than one frame to complete, especially on slower processors; this can be compensated for by maintaining a count of new screens drawn and cross-referencing that to the BIOS timer count periodically, accelerating the overall pace of the animation (moving farther each time and the like) if updates are happening too slowly.

Enter the Split Screen

So far, I've discussed page flipping in 640x350 mode. There's a reason for that: 640x350 is the highest-resolution standard mode in which there's enough display memory for two full pages on a standard VGA. It's possible to program the VGA to a non-standard 640x400 mode and still have two full pages, but that's pretty much the limit. One 640x480 page takes 38,400 bytes of display memory, and clearly there isn't enough room in 64 K of display memory for two of those monster pages.

And yet, 640x480 is a wonderful mode in many ways. It offers a 1:1 aspect ratio (square pixels), and it provides by far the best resolution of any 16-color mode. Surely there's some way to bring the visual appeal of page flipping to this mode?

Surely there is—but it's an odd solution indeed. The VGA has a feature, known as the split screen, that allows you to force the offset from which the VGA fetches video data back to 0 after any desired scan line. For example, you can program the VGA to scan through display memory as usual until it finishes scan line number 338, and then get the first byte of information for scan line number 339 from offset 0 in display memory.

That, in turn, allows us to divvy up display memory into three areas, as shown in Figure 44.2. The area from 0 to 11,279 is reserved for the split screen, the area from 11,280 to 38,399 is used for page 0, and the area from 38,400 to 65,519 is used for page 1. This allows page flipping to be performed in the top 339 scan lines (about 70 percent) of the screen, and leaves the bottom 141 scan lines for non-animation purposes, such as showing scores, instructions, statuses, and suchlike. (Note that the allocation of display memory and number of scan lines are dictated by the desire to have as many page-flipped scan lines as possible; you may, if you wish, have fewer page-flipped lines and reserve part of the bitmap for other uses, such as off-screen storage for images.)

Figure 44.2  Memory allocation for mode 12h page flipping.

The sample program for this chapter uses the split screen and page flipping exactly as described above. The playfield through which the object bounces is the page-flipped portion of the screen, and the rectangle at the bottom containing the bounce count and the instructions is the split (that is, not animatable) portion of the screen. Of course, to the user it all looks like one screen. There are no visible boundaries between the two unless you choose to create them.

Very few animation applications use the entire screen for animation. If you can get by with 339 scan lines of animation, split-screen page flipping gives you the best combination of square pixels and high resolution possible on a standard VGA.

So. Is VGA animation worth all the fuss? Mais oui. Run the sample program; if you've never seen aggressive VGA animation before, you'll be amazed at how smooth it can be. Not every square millimeter of every animated screen must be in constant motion. Most graphics screens need a little quiet space to display scores, coordinates, file names, or (if all else fails) company logos. If you don't tell the user he's/she's only getting 339 scan lines of animation, he'll/she'll probably never know.