879 lines
39 KiB
Markdown
879 lines
39 KiB
Markdown
---
|
||
title: Michael Abrash's Graphics Programming Black Book, Special Edition
|
||
author: Michael Abrash
|
||
date: '1997-07-01'
|
||
identifier:
|
||
- scheme: ISBN
|
||
text: 1576101746
|
||
publisher: The Coriolis Group
|
||
category: 'Web and Software Development: Game Development,Web and Software Development:
|
||
Graphics and Multimedia Development'
|
||
chapter: '45'
|
||
pages: 839-857
|
||
---
|
||
|
||
## Chapter 45 -- Dog Hair and Dirty Rectangles
|
||
|
||
### Different Angles on Animation
|
||
|
||
We brought our pets with us when we moved to Seattle. At about the same
|
||
time, our Golden Retriever, Sam, observed his third birthday. Sam is
|
||
relatively intelligent, in the sense that he is clearly smarter than a
|
||
banana slug, although if he were in the same room with Jeff Duntemann's
|
||
dog Mr. Byte, there's a reasonable chance that he would mistake Mr. Byte
|
||
for something edible (a category that includes rocks, socks, and a
|
||
surprising number of things too disgusting to mention), and Jeff would
|
||
have to find a new source of things to write about.
|
||
|
||
But that's not important now. What is important is that—and I am not
|
||
making this up—this morning I managed to find the one pair of socks Sam
|
||
hadn't chewed holes in. And what's even more important is that after we
|
||
moved and Sam turned three, he calmed down amazingly. We had been
|
||
waiting for this magic transformation since Sam turned one, the age at
|
||
which most puppies turn into normal dogs who lie around a lot, waking up
|
||
to eat their Science Diet (motto, "The dog food that costs more than the
|
||
average neurosurgeon makes in a year") before licking themselves in
|
||
embarrassing places and going back to sleep. When Sam turned one and
|
||
remained hopelessly out of control we said, "Goldens take two years to
|
||
calm down," as if we had a clue. When he turned two and remained
|
||
undeniably Sam we said, "Any day now." By the time he turned three, we
|
||
were reduced to figuring that it was only about seven more years until
|
||
he expired, at which point we might be able to take all the fur he had
|
||
shed in his lifetime and weave ourselves some clothes without holes in
|
||
them, or quite possibly a house.
|
||
|
||
But miracle of miracles, we moved, and Sam instantly turned into the dog
|
||
we thought we'd gotten when we forked over \$500—calm, sweet, and
|
||
obedient. Weeks went by, and Sam was, if anything, better than ever.
|
||
Clearly, the change was permanent.
|
||
|
||
And then we took Sam to the vet for his annual check-up and found that
|
||
he had an ear infection. Thanks to the wonders of modern animal
|
||
medicine, a \$5 bottle of liquid restored his health in just two days.
|
||
And with his health, we got, as a bonus, the old Sam. You see, Sam
|
||
hadn't changed. He was just tired from being sick. Now he once again
|
||
joyously knocks down any stranger who makes the mistake of glancing in
|
||
his direction, and will, quite possibly, be booked any day now on
|
||
suspicion of homicide by licking.
|
||
|
||
### Plus ça Change
|
||
|
||
Okay, you give up. What exactly does this have to do with graphics? I'm
|
||
glad you asked. The lesson to be learned from Sam, The Dog With A Brain
|
||
The Size Of A Walnut, is that while things may *look* like they've
|
||
changed, in fact they often haven't. Take VGA performance. If you buy a
|
||
486 with a SuperVGA, you'll get performance that knocks your socks off,
|
||
especially if you run Windows. Things are liable to be so fast that
|
||
you'll figure the SuperVGA has to deserve some of the credit. Well,
|
||
maybe it does if it's a local-bus VGA. But maybe it doesn't, even if it
|
||
is local bus—and it certainly doesn't if it's an ISA bus VGA, because no
|
||
ISA bus VGA can run faster than about 300 nanoseconds per access, and
|
||
VGAs capable of that speed have been common for at least a couple of
|
||
years now.
|
||
|
||
Your 486 VGA system is fast almost entirely because it has a 486 in it.
|
||
(486 systems with graphics accelerators such as the ATI Ultra or Diamond
|
||
Stealth are another story altogether.) Underneath it all, the VGA is
|
||
still painfully slow—and if you have an old VGA or IBM's original PS/2
|
||
motherboard VGA, it's incredibly slow. The fastest ISA-bus VGA around is
|
||
two to twenty times slower than system memory, and the slowest VGA
|
||
around is as much as 100 times slower. In the old days, the rule was,
|
||
"Display memory is slow, and should be avoided." Nowadays, the rule is,
|
||
"Display memory is not quite so slow, but should still be avoided."
|
||
|
||
So, as I say, sometimes things don't change. Of course, sometimes they
|
||
do change. For example, in just 49 dog years, I fully expect to own at
|
||
least one pair of underwear without a single hole in it. Which brings
|
||
us, deus ex machina and the creek don't rise, to yet another animation
|
||
method: dirty-rectangle animation.
|
||
|
||
### VGA Access Times
|
||
|
||
Actually, before we get to dirty rectangles, I'd like to take you
|
||
through a quick refresher on VGA memory and I/O access times. I want to
|
||
do this partly because the slow access times of the VGA make
|
||
dirty-rectangle animation particularly attractive, and partly as a
|
||
public service, because even I was shocked by the results of some I/O
|
||
performance tests I recently ran.
|
||
|
||
Table 45.1 shows the results of the aforementioned I/O performance
|
||
tests, as run on two 486/33 SuperVGA systems under the Phar Lap
|
||
386|DOS-Extender. (The systems and VGAs are unnamed because this is a
|
||
not-very-scientific spot test, and I don't want to unfairly malign, say,
|
||
a VGA whose only sin is being plugged into a lousy motherboard, or vice
|
||
versa.) Under Phar Lap, 32-bit protected-mode apps run with full I/O
|
||
privileges, meaning that the `OUT` instructions I measured had the
|
||
best official cycle times possible on the 486: 10 cycles. `OUT`
|
||
officially takes 16 cycles in real mode on a 486, and officially takes a
|
||
mind-boggling 30 cycles in protected mode if running *without* full I/O
|
||
privileges (as is normally the case for protected-mode applications).
|
||
Basically, I/O is just plain slow on a 486.
|
||
|
||
As slow as 30 or even 10 cycles is for an `OUT`, one could only wish
|
||
that VGA I/O were actually that fast. The fastest measured `OUT` to a
|
||
VGA in Table 45.1 is 26 cycles, and the slowest is 126—this for an
|
||
operation that's *supposed* to take 10 cycles. To put this in context,
|
||
`MUL` takes only 13 to 42 cycles, and a normal `MOV` to or from
|
||
system memory takes exactly one cycle on the 486. In short, `OUT`s to
|
||
VGAs are as much as 100 times slower than normal memory accesses, and
|
||
are generally two to four times slower than even display memory
|
||
accesses, although there are exceptions.
|
||
|
||
Of course, VGA display memory has its own performance problems. The
|
||
fastest ISA bus VGA can, at best, support sustained write times of about
|
||
10 cycles per word-sized write on a 486/33; 15 or 20 cycles is more
|
||
common, even for relatively fast SuperVGAs; the worst case I've seen is
|
||
65 cycles per byte. However, intermittent writes, mixed with a lot of
|
||
register and cache-only code, can effectively execute in one cycle,
|
||
thanks to the caching design of many VGAs and the 486's 4-deep write
|
||
buffer, which stores pending writes while the CPU continues executing
|
||
instructions. Display memory reads tend to take longer, because
|
||
coprocessing isn't possible—one microsecond is a reasonable rule of
|
||
thumb for VGA reads, although there's considerable variation. So VGA
|
||
memory tends not to be as bad as VGA I/O, but lord knows it isn't
|
||
*good*.
|
||
|
||
**OUT Time in Microseconds and Cycles**
|
||
|
||
| OUT Instruction | Official Time | 486 #1/16-bit VGA #1 | 486 #2/16-bit VGA #2 |
|
||
|-----------------|---------------|----------------------|----------------------|
|
||
| OUT DX,AL repeated 1,000 times nonstop (maximum byte access) | 0.300s 10 cycles | 2.546s 84 cycles | 0.813s 27 cycles |
|
||
| OUT DX,AX repeated 1,000 times nonstop (maximum word access) | 0.300s 10 cycles | 3.820s 126 cycles | 1.066s 35 cycles |
|
||
| OUT DX,AL repeated 1,000 times, but interspersed with MULs (random byte access) | 0.300s 10 cycles | 1.610s 53 cycles | 0.780s 26 cycles |
|
||
| OUT DX,AX repeated 1,000 times, but interspersed with MULs (random word access) | 0.300s 10 cycles | 2.830s 93 cycles | 1.010s 33 cycles |
|
||
|
||
Table: Table 45.1 Results of I/O performance tests run under the Phar
|
||
Lap386|DOS-Extender.
|
||
|
||
> 
|
||
> `OUT`s, in general, are lousy on the 486 (and to think they only took
|
||
> three cycles on the 286!). `OUT`s to VGAs are particularly lousy.
|
||
> Display memory performance is pretty poor, especially for reads. The
|
||
> conclusions are obvious, I would hope. Structure your graphics code,
|
||
> and, in general, all 486 code, to avoid `OUT`s.
|
||
|
||
For graphics, this especially means using write mode 3 rather than the
|
||
bit-mask register. When you must use the bit mask, arrange drawing so
|
||
that you can set the bit mask once, then do a lot of drawing with that
|
||
mask. For example, draw a whole edge at once, then the middle, then the
|
||
other edge, rather than setting the bit mask several times on each scan
|
||
line to draw the edge and middle bytes together. Don't read from display
|
||
memory if you don't have to. Write each pixel once and only once.
|
||
|
||
It is indeed a strange concept: The key to fast graphics is staying away
|
||
from the graphics adapter as much as possible.
|
||
|
||
### Dirty-Rectangle Animation
|
||
|
||
The relative slowness of VGA hardware is part of the appeal of the
|
||
technique that I call "dirty-rectangle" animation, in which a complete
|
||
copy of the contents of display memory is maintained in offscreen system
|
||
(nondisplay) memory. All drawing is done to this system buffer. As
|
||
offscreen drawing is done, a list is maintained of the bounding
|
||
rectangles for the drawn-to areas; these are the *dirty rectangles*,
|
||
"dirty" in the sense that that have been altered and no longer match the
|
||
contents of the screen. After all drawing for a frame is completed, all
|
||
the dirty rectangles for that frame are copied to the screen in a burst,
|
||
and then the cycle of off-screen drawing begins again.
|
||
|
||
Why, exactly, would we want to go through all this complication, rather
|
||
than simply drawing to the screen in the first place? The reason is
|
||
visual quality. If we were to do all our drawing directly to the screen,
|
||
there'd be a lot of flicker as objects were erased and then redrawn.
|
||
Similarly, overlapped drawing done with the painter's algorithm (in
|
||
which farther objects are drawn first, so that nearer objects obscure
|
||
them) would flicker as farther objects were visible for short periods.
|
||
With dirty-rectangle animation, only the finished pixels for any given
|
||
frame ever appear on the screen; intermediate results are never visible.
|
||
Figure 45.1 illustrates the visual problems associated with drawing
|
||
directly to the screen; Figure 45.2 shows how dirty-rectangle animation
|
||
solves these problems.
|
||
|
||

|
||
|
||

|
||
|
||
#### So Why Not Use Page Flipping?
|
||
|
||
Well, then, if we want good visual quality, why not use page flipping?
|
||
For one thing, not all adapters and all modes support page flipping. The
|
||
CGA and MCGA don't, and neither do the VGA's 640x480 16-color or 320x200
|
||
256-color modes, or many SuperVGA modes. In contrast, *all* adapters
|
||
support dirty-rectangle animation. Another advantage of dirty-rectangle
|
||
animation is that it's generally faster. While it may seem strange that
|
||
it would be faster to draw off-screen and then copy the result to the
|
||
screen, that is often the case, because dirty-rectangle animation
|
||
usually reduces the number of times the VGA's hardware needs to be
|
||
touched, especially in 256-color modes.
|
||
|
||
This reduction comes about because when dirty rectangles are erased,
|
||
it's done in system memory, not in display memory, and since most
|
||
objects move a good deal less than their full width (that is, the new
|
||
and old positions overlap), display memory is written to fewer times
|
||
than with page flipping. (In 16-color modes, this is not necessarily the
|
||
case, because of the parallelism obtained from the VGA's planar
|
||
hardware.) Also, read/modify/write operations are performed in fast
|
||
system memory rather than slow display memory, so display memory rarely
|
||
needs to be read. This is particularly good because display memory is
|
||
generally even slower for reads than for writes.
|
||
|
||
Also, page flipping wastes a good deal of time waiting for the page to
|
||
flip at the end of the frame. Dirty-rectangle animation never needs to
|
||
wait for anything because partially drawn images are never present in
|
||
display memory. Actually, in one sense, partially drawn images are
|
||
sometimes present because it's possible for a rectangle to be partially
|
||
drawn when the scanning raster beam reaches that part of the screen.
|
||
This causes the rectangle to appear partially drawn for one frame,
|
||
producing a phenomenon I call "shearing." Fortunately, shearing tends
|
||
not to be particularly distracting, especially for fairly small images,
|
||
but it can be a problem when copying large areas. This is one area in
|
||
which dirty-rectangle animation falls short of page flipping, because
|
||
page flipping has perfect display quality, never showing anything other
|
||
than a completely finished frame. Similarly, dirty-rectangle copying may
|
||
take two or more frame times to finish, so even if shearing doesn't
|
||
happen, it's still possible to have the images in the various dirty
|
||
rectangles show up non-simultaneously. In my experience, this latter
|
||
phenomenon is not a serious problem, but do be aware of it.
|
||
|
||
### Dirty Rectangles in Action
|
||
|
||
Listing 45.1 demonstrates dirty-rectangle animation. This is a very
|
||
simple implementation, in several respects. For one thing, it's written
|
||
entirely in C, and animation fairly cries out for assembly language. For
|
||
another thing, it uses far pointers, which C often handles with less
|
||
than optimal efficiency, especially because I haven't used library
|
||
functions to copy and fill memory. (I did this so the code would work in
|
||
any memory model.) Also, Listing 45.1 doesn't attempt to coalesce
|
||
rectangles so as to perform a minimum number of display-memory accesses;
|
||
instead, it copies each dirty rectangle to the screen, even if it
|
||
overlaps with another rectangle, so some pixels are copied multiple
|
||
times. Listing 45.1 runs pretty well, considering all of its failings;
|
||
on my 486/33, 10 11x11 images animate at a very respectable clip.
|
||
|
||
**LISTING 45.1 L45-1.C**
|
||
|
||
```c
|
||
/* Sample simple dirty-rectangle animation program. Doesn't attempt to coalesce
|
||
rectangles to minimize display memory accesses. Not even vaguely optimized!
|
||
Tested with Borland C++ in the small model. */
|
||
|
||
#include <stdlib.h>
|
||
#include <conio.h>
|
||
#include <alloc.h>
|
||
#include <memory.h>
|
||
#include <dos.h>
|
||
|
||
#define SCREEN_WIDTH 320
|
||
#define SCREEN_HEIGHT 200
|
||
#define SCREEN_SEGMENT 0xA000
|
||
|
||
/* Describes a rectangle */
|
||
typedef struct {
|
||
int Top;
|
||
int Left;
|
||
int Right;
|
||
int Bottom;
|
||
} Rectangle;
|
||
|
||
/* Describes an animated object */
|
||
typedef struct {
|
||
int X; /* upper left corner in virtual bitmap */
|
||
int Y;
|
||
int XDirection; /* direction and distance of movement */
|
||
int YDirection;
|
||
} Entity;
|
||
|
||
/* Storage used for dirty rectangles */
|
||
#define MAX_DIRTY_RECTANGLES 100
|
||
int NumDirtyRectangles;
|
||
Rectangle DirtyRectangles[MAX_DIRTY_RECTANGLES];
|
||
|
||
/* If set to 1, ignore dirty rectangle list and copy the whole screen. */
|
||
int DrawWholeScreen = 0;
|
||
|
||
/* Pixels for image we'll animate */
|
||
#define IMAGE_WIDTH 11
|
||
#define IMAGE_HEIGHT 11
|
||
char ImagePixels[] = {
|
||
15,15,15, 9, 9, 9, 9, 9,15,15,15,
|
||
15,15, 9, 9, 9, 9, 9, 9, 9,15,15,
|
||
15, 9, 9,14,14,14,14,14, 9, 9,15,
|
||
9, 9,14,14,14,14,14,14,14, 9, 9,
|
||
9, 9,14,14,14,14,14,14,14, 9, 9,
|
||
9, 9,14,14,14,14,14,14,14, 9, 9,
|
||
9, 9,14,14,14,14,14,14,14, 9, 9,
|
||
9, 9,14,14,14,14,14,14,14, 9, 9,
|
||
15, 9, 9,14,14,14,14,14, 9, 9,15,
|
||
15,15, 9, 9, 9, 9, 9, 9, 9,15,15,
|
||
15,15,15, 9, 9, 9, 9, 9,15,15,15,
|
||
};
|
||
/* animated entities */
|
||
#define NUM_ENTITIES 10
|
||
Entity Entities[NUM_ENTITIES];
|
||
|
||
/* pointer to system buffer into which we'll draw */
|
||
char far *SystemBufferPtr;
|
||
|
||
/* pointer to screen */
|
||
char far *ScreenPtr;
|
||
|
||
void EraseEntities(void);
|
||
void CopyDirtyRectanglesToScreen(void);
|
||
void DrawEntities(void);
|
||
|
||
void main()
|
||
{
|
||
int i, XTemp, YTemp;
|
||
unsigned int TempCount;
|
||
char far *TempPtr;
|
||
union REGS regs;
|
||
/* Allocate memory for the system buffer into which we'll draw */
|
||
if (!(SystemBufferPtr = farmalloc((unsigned int)SCREEN_WIDTH*
|
||
SCREEN_HEIGHT))) {
|
||
printf("Couldn't get memory\n");
|
||
exit(1);
|
||
}
|
||
/* Clear the system buffer */
|
||
TempPtr = SystemBufferPtr;
|
||
for (TempCount = ((unsigned)SCREEN_WIDTH*SCREEN_HEIGHT); TempCount--; ) {
|
||
*TempPtr++ = 0;
|
||
}
|
||
/* Point to the screen */
|
||
ScreenPtr = MK_FP(SCREEN_SEGMENT, 0);
|
||
|
||
/* Set up the entities we'll animate, at random locations */
|
||
randomize();
|
||
for (i = 0; i < NUM_ENTITIES; i++) {
|
||
Entities[i].X = random(SCREEN_WIDTH - IMAGE_WIDTH);
|
||
Entities[i].Y = random(SCREEN_HEIGHT - IMAGE_HEIGHT);
|
||
Entities[i].XDirection = 1;
|
||
Entities[i].YDirection = -1;
|
||
}
|
||
/* Set 320x200 256-color graphics mode */
|
||
regs.x.ax = 0x0013;
|
||
int86(0x10, ®s, ®s);
|
||
|
||
/* Loop and draw until a key is pressed */
|
||
do {
|
||
/* Draw the entities to the system buffer at their current locations,
|
||
updating the dirty rectangle list */
|
||
DrawEntities();
|
||
|
||
/* Draw the dirty rectangles, or the whole system buffer if
|
||
appropriate */
|
||
CopyDirtyRectanglesToScreen();
|
||
|
||
/* Reset the dirty rectangle list to empty */
|
||
NumDirtyRectangles = 0;
|
||
|
||
/* Erase the entities in the system buffer at their old locations,
|
||
updating the dirty rectangle list */
|
||
EraseEntities();
|
||
|
||
/* Move the entities, bouncing off the edges of the screen */
|
||
for (i = 0; i < NUM_ENTITIES; i++) {
|
||
XTemp = Entities[i].X + Entities[i].XDirection;
|
||
YTemp = Entities[i].Y + Entities[i].YDirection;
|
||
if ((XTemp < 0) || ((XTemp + IMAGE_WIDTH) > SCREEN_WIDTH)) {
|
||
Entities[i].XDirection = -Entities[i].XDirection;
|
||
XTemp = Entities[i].X + Entities[i].XDirection;
|
||
}
|
||
if ((YTemp < 0) || ((YTemp + IMAGE_HEIGHT) > SCREEN_HEIGHT)) {
|
||
Entities[i].YDirection = -Entities[i].YDirection;
|
||
YTemp = Entities[i].Y + Entities[i].YDirection;
|
||
}
|
||
Entities[i].X = XTemp;
|
||
Entities[i].Y = YTemp;
|
||
}
|
||
|
||
} while (!kbhit());
|
||
getch(); /* clear the keypress */
|
||
/* Back to text mode */
|
||
regs.x.ax = 0x0003;
|
||
int86(0x10, ®s, ®s);
|
||
}
|
||
/* Draw entities at current locations, updating dirty rectangle list. */
|
||
void DrawEntities()
|
||
{
|
||
int i, j, k;
|
||
char far *RowPtrBuffer;
|
||
char far *TempPtrBuffer;
|
||
char far *TempPtrImage;
|
||
for (i = 0; i < NUM_ENTITIES; i++) {
|
||
/* Remember the dirty rectangle info for this entity */
|
||
if (NumDirtyRectangles >= MAX_DIRTY_RECTANGLES) {
|
||
/* Too many dirty rectangles; just redraw the whole screen */
|
||
DrawWholeScreen = 1;
|
||
} else {
|
||
/* Remember this dirty rectangle */
|
||
DirtyRectangles[NumDirtyRectangles].Left = Entities[i].X;
|
||
DirtyRectangles[NumDirtyRectangles].Top = Entities[i].Y;
|
||
DirtyRectangles[NumDirtyRectangles].Right =
|
||
Entities[i].X + IMAGE_WIDTH;
|
||
DirtyRectangles[NumDirtyRectangles++].Bottom =
|
||
Entities[i].Y + IMAGE_HEIGHT;
|
||
}
|
||
/* Point to the destination in the system buffer */
|
||
RowPtrBuffer = SystemBufferPtr + (Entities[i].Y * SCREEN_WIDTH) +
|
||
Entities[i].X;
|
||
/* Point to the image to draw */
|
||
TempPtrImage = ImagePixels;
|
||
/* Copy the image to the system buffer */
|
||
for (j = 0; j < IMAGE_HEIGHT; j++) {
|
||
/* Copy a row */
|
||
for (k = 0, TempPtrBuffer = RowPtrBuffer; k < IMAGE_WIDTH; k++) {
|
||
*TempPtrBuffer++ = *TempPtrImage++;
|
||
}
|
||
/* Point to the next system buffer row */
|
||
RowPtrBuffer += SCREEN_WIDTH;
|
||
}
|
||
}
|
||
}
|
||
/* Copy the dirty rectangles, or the whole system buffer if appropriate,
|
||
to the screen. */
|
||
void CopyDirtyRectanglesToScreen()
|
||
{
|
||
int i, j, k, RectWidth, RectHeight;
|
||
unsigned int TempCount;
|
||
unsigned int Offset;
|
||
char far *TempPtrScreen;
|
||
char far *TempPtrBuffer;
|
||
|
||
if (DrawWholeScreen) {
|
||
/* Just copy the whole buffer to the screen */
|
||
DrawWholeScreen = 0;
|
||
TempPtrScreen = ScreenPtr;
|
||
TempPtrBuffer = SystemBufferPtr;
|
||
for (TempCount = ((unsigned)SCREEN_WIDTH*SCREEN_HEIGHT); TempCount--; ) {
|
||
*TempPtrScreen++ = *TempPtrBuffer++;
|
||
}
|
||
} else {
|
||
/* Copy only the dirty rectangles */
|
||
for (i = 0; i < NumDirtyRectangles; i++) {
|
||
/* Offset in both system buffer and screen of image */
|
||
Offset = (unsigned int) (DirtyRectangles[i].Top * SCREEN_WIDTH) +
|
||
DirtyRectangles[i].Left;
|
||
/* Dimensions of dirty rectangle */
|
||
RectWidth = DirtyRectangles[i].Right - DirtyRectangles[i].Left;
|
||
RectHeight = DirtyRectangles[i].Bottom - DirtyRectangles[i].Top;
|
||
/* Copy a dirty rectangle */
|
||
for (j = 0; j < RectHeight; j++) {
|
||
|
||
/* Point to the start of row on screen */
|
||
TempPtrScreen = ScreenPtr + Offset;
|
||
|
||
/* Point to the start of row in system buffer */
|
||
TempPtrBuffer = SystemBufferPtr + Offset;
|
||
|
||
/* Copy a row */
|
||
for (k = 0; k < RectWidth; k++) {
|
||
*TempPtrScreen++ = *TempPtrBuffer++;
|
||
}
|
||
/* Point to the next row */
|
||
Offset += SCREEN_WIDTH;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/* Erase the entities in the system buffer at their current locations,
|
||
updating the dirty rectangle list. */
|
||
void EraseEntities()
|
||
{
|
||
int i, j, k;
|
||
char far *RowPtr;
|
||
char far *TempPtr;
|
||
|
||
for (i = 0; i < NUM_ENTITIES; i++) {
|
||
/* Remember the dirty rectangle info for this entity */
|
||
if (NumDirtyRectangles >= MAX_DIRTY_RECTANGLES) {
|
||
/* Too many dirty rectangles; just redraw the whole screen */
|
||
DrawWholeScreen = 1;
|
||
} else {
|
||
/* Remember this dirty rectangle */
|
||
DirtyRectangles[NumDirtyRectangles].Left = Entities[i].X;
|
||
DirtyRectangles[NumDirtyRectangles].Top = Entities[i].Y;
|
||
DirtyRectangles[NumDirtyRectangles].Right =
|
||
Entities[i].X + IMAGE_WIDTH;
|
||
DirtyRectangles[NumDirtyRectangles++].Bottom =
|
||
Entities[i].Y + IMAGE_HEIGHT;
|
||
}
|
||
/* Point to the destination in the system buffer */
|
||
RowPtr = SystemBufferPtr + (Entities[i].Y*SCREEN_WIDTH) + Entities[i].X;
|
||
|
||
/* Clear the entity's rectangle */
|
||
for (j = 0; j < IMAGE_HEIGHT; j++) {
|
||
/* Clear a row */
|
||
for (k = 0, TempPtr = RowPtr; k < IMAGE_WIDTH; k++) {
|
||
*TempPtr++ = 0;
|
||
}
|
||
/* Point to the next row */
|
||
RowPtr += SCREEN_WIDTH;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
One point I'd like to make is that although the system-memory buffer in
|
||
Listing 45.1 has exactly the same dimensions as the screen bitmap,
|
||
that's not a requirement, and there are some good reasons not to make
|
||
the two the same size. For example, if the system buffer is bigger than
|
||
the area displayed on the screen, it's possible to pan the visible area
|
||
around the system buffer. Or, alternatively, the system buffer can be
|
||
just the size of a desired window, representing a window into a larger,
|
||
virtual buffer. We could then draw the desired portion of the virtual
|
||
bitmap into the system-memory buffer, then copy the buffer to the
|
||
screen, and the effect will be of having panned the window to the new
|
||
location.
|
||
|
||
> 
|
||
> Another argument in favor of a small viewing window is that it restricts
|
||
> the amount of display memory actually drawn to. Restricting the display
|
||
> memory used for animation reduces the total number of display-memory
|
||
> accesses, which in turn boosts overall performance; it also improves the
|
||
> performance and appearance of panning, in which the whole window has to
|
||
> be redrawn or copied.
|
||
|
||
If you keep a close watch, you'll notice that many high-performance
|
||
animation games similarly restrict their full-featured animation area to
|
||
a relatively small region. Often, it's hard to tell that this is the
|
||
case, because the animation region is surrounded by flashy digitized
|
||
graphics and by items such as scoreboards and status screens, but look
|
||
closely and see if the animation region in your favorite game isn't
|
||
smaller than you thought.
|
||
|
||
### Hi-Res VGA Page Flipping
|
||
|
||
On a standard VGA, hi-res mode is mode 12H, which offers 640x480
|
||
resolution with 16 colors. That's a nice mode, with plenty of pixels,
|
||
and square ones at that, but it lacks one thing—page flipping. The
|
||
problem is that the mode 12H bitmap is 150 K in size, and the standard
|
||
VGA has only 256 K total, too little memory for two of those monster
|
||
mode 12H pages. With only one page, flipping is obviously out of the
|
||
question, and without page flipping, top-flight, hi-res animation can't
|
||
be implemented. The standard fallback is to use the EGA's hi-res mode,
|
||
mode 10H (640x350, 16 colors) for page flipping, but this mode is less
|
||
than ideal for a couple of reasons: It offers sharply lower vertical
|
||
resolution, and it's lousy for handling scaled-up CGA graphics, because
|
||
the vertical resolution is a fractional multiple—1.75 times, to be
|
||
exact—of that of the CGA. CGA resolution may not seem important these
|
||
days, but many images were originally created for the CGA, as were many
|
||
graphics packages and games, and it's at least convenient to be able to
|
||
handle CGA graphics easily. Then, too, 640x350 is also a poor multiple
|
||
of the 200 scan lines of the popular 320x200 256-color mode 13H of the
|
||
VGA.
|
||
|
||
There are a couple of interesting, if imperfect, solutions to the
|
||
problem of hi-res page flipping. One is to use the split screen to
|
||
enable page flipping only in the top two-thirds of the screen; see the
|
||
previous chapter for details, and for details on the mechanics of page
|
||
flipping generally. This doesn't address the CGA problem, but it does
|
||
yield square pixels and a full 640x480 screen resolution, although not
|
||
all those pixels are flippable and thus animatable.
|
||
|
||
A second solution is to program the screen to a 640x400 mode. Such a
|
||
mode uses almost every byte of display memory (64,000 bytes, actually;
|
||
you could add another few lines, if you really wanted to), and thereby
|
||
provides the highest resolution possible on the VGA for a fully
|
||
page-flipped display. It maps well to CGA and mode 13H resolutions,
|
||
being either identical or double in both dimensions. As an added
|
||
benefit, it offers an easy-on-the-eyes 70-Hz frame rate, as opposed to
|
||
the 60 Hz that is the best that mode 12H can offer, due to the design of
|
||
standard VGA monitors. Best of all, perhaps, is that 640x400 16-color
|
||
mode is easy to set up.
|
||
|
||
The key to 640x400 mode is understanding that on a VGA, mode 10H
|
||
(640x350) is, at heart, a 400-scan-line mode. What I mean by that is
|
||
that in mode 10H, the Vertical Total register, which controls the total
|
||
number of scan lines, both displayed and nondisplayed, is set to 447,
|
||
exactly the same as in the VGA's text modes, which do in fact support
|
||
400 scan lines. A properly sized and centered display is achieved in
|
||
mode 10H by setting the polarity of the sync pulses to tell the monitor
|
||
to scan vertically at a faster rate (to make fewer lines fill the
|
||
screen), by starting the overscan after 350 lines, and by setting the
|
||
vertical sync and blanking pulses appropriately for the faster vertical
|
||
scanning rate. Changing those settings is all that's required to turn
|
||
mode 10H into a 640x400 mode, and that's easy to do, as illustrated by
|
||
Listing 45.2, which provides mode set code for 640x400 mode.
|
||
|
||
**LISTING 45.2 L45-2.C**
|
||
|
||
```c
|
||
/* Mode set routine for VGA 640x400 16-color mode. Tested with
|
||
Borland C++ in C compilation mode. */
|
||
|
||
#include <dos.h>
|
||
|
||
void Set640x400()
|
||
{
|
||
union REGS regset;
|
||
|
||
/* First, set to standard 640x350 mode (mode 10h) */
|
||
regset.x.ax = 0x0010;
|
||
int86(0x10, ®set, ®set);
|
||
|
||
/* Modify the sync polarity bits (bits 7 & 6) of the
|
||
Miscellaneous Output register (readable at 0x3CC, writable at
|
||
0x3C2) to select the 400-scan-line vertical scanning rate */
|
||
outp(0x3C2, ((inp(0x3CC) & 0x3F) | 0x40));
|
||
|
||
/* Now, tweak the registers needed to convert the vertical
|
||
timings from 350 to 400 scan lines */
|
||
outpw(0x3D4, 0x9C10); /* adjust the Vertical Sync Start register
|
||
for 400 scan lines */
|
||
outpw(0x3D4, 0x8E11); /* adjust the Vertical Sync End register
|
||
for 400 scan lines */
|
||
outpw(0x3D4, 0x8F12); /* adjust the Vertical Display End
|
||
register for 400 scan lines */
|
||
outpw(0x3D4, 0x9615); /* adjust the Vertical Blank Start
|
||
register for 400 scan lines */
|
||
outpw(0x3D4, 0xB916); /* adjust the Vertical Blank End register
|
||
for 400 scan lines */
|
||
}
|
||
```
|
||
|
||
In 640x400, 16-color mode, page 0 runs from offset 0 to offset 31,999
|
||
(7CFFH), and page 1 runs from offset 32,000 (7D00H) to 63,999 (0F9FFH).
|
||
Page 1 is selected by programming the Start Address registers (CRTC
|
||
registers 0CH, the high 8 bits, and 0DH, the low 8 bits) to 7D00H.
|
||
Actually, because the low byte of the start address is 0 for both pages,
|
||
you can page flip simply by writing 0 or 7DH to the Start Address High
|
||
register (CRTC register 0CH); this has the benefit of eliminating a
|
||
nasty class of potential synchronization bugs that can arise when both
|
||
registers must be set. Listing 45.3 illustrates simple 640x400 page
|
||
flipping.
|
||
|
||
**LISTING 45.3 L45-3.C**
|
||
|
||
```c
|
||
/* Sample program to exercise VGA 640x400 16-color mode page flipping, by
|
||
drawing a horizontal line at the top of page 0 and another at bottom of page 1,
|
||
then flipping between them once every 30 frames. Tested with Borland C++,
|
||
in C compilation mode. */
|
||
|
||
#include <dos.h>
|
||
#include <conio.h>
|
||
|
||
#define SCREEN_SEGMENT 0xA000
|
||
#define SCREEN_HEIGHT 400
|
||
#define SCREEN_WIDTH_IN_BYTES 80
|
||
#define INPUT_STATUS_1 0x3DA /* color-mode address of Input Status 1
|
||
register */
|
||
/* The page start addresses must be even multiples of 256, because page
|
||
flipping is performed by changing only the upper start address byte */
|
||
#define PAGE_0_START 0
|
||
#define PAGE_1_START (400*SCREEN_WIDTH_IN_BYTES)
|
||
|
||
void main(void);
|
||
void Wait30Frames(void);
|
||
extern void Set640x400(void);
|
||
|
||
void main()
|
||
{
|
||
int i;
|
||
unsigned int far *ScreenPtr;
|
||
union REGS regset;
|
||
|
||
Set640x400(); /* set to 640x400 16-color mode */
|
||
|
||
/* Point to first line of page 0 and draw a horizontal line across screen */
|
||
FP_SEG(ScreenPtr) = SCREEN_SEGMENT;
|
||
FP_OFF(ScreenPtr) = PAGE_0_START;
|
||
for (i=0; i<(SCREEN_WIDTH_IN_BYTES/2); i++) *ScreenPtr++ = 0xFFFF;
|
||
|
||
/* Point to last line of page 1 and draw a horizontal line across screen */
|
||
FP_OFF(ScreenPtr) =
|
||
PAGE_1_START + ((SCREEN_HEIGHT-1)*SCREEN_WIDTH_IN_BYTES);
|
||
for (i=0; i<(SCREEN_WIDTH_IN_BYTES/2); i++) *ScreenPtr++ = 0xFFFF;
|
||
|
||
/* Now flip pages once every 30 frames until a key is pressed */
|
||
do {
|
||
Wait30Frames();
|
||
|
||
/* Flip to page 1 */
|
||
outpw(0x3D4, 0x0C | ((PAGE_1_START >> 8) << 8));
|
||
|
||
Wait30Frames();
|
||
|
||
/* Flip to page 0 */
|
||
outpw(0x3D4, 0x0C | ((PAGE_0_START >> 8) << 8));
|
||
} while (kbhit() == 0);
|
||
|
||
getch(); /* clear the key press */
|
||
|
||
/* Return to text mode and exit */
|
||
regset.x.ax = 0x0003; /* AL = 3 selects 80x25 text mode */
|
||
int86(0x10, ®set, ®set);
|
||
}
|
||
|
||
void Wait30Frames()
|
||
{
|
||
int i;
|
||
|
||
for (i=0; i<30; i++) {
|
||
/* Wait until we're not in vertical sync, so we can catch leading edge */
|
||
while ((inp(INPUT_STATUS_1) & 0x08) != 0) ;
|
||
/* Wait until we are in vertical sync */
|
||
while ((inp(INPUT_STATUS_1) & 0x08) == 0) ;
|
||
}
|
||
}
|
||
```
|
||
|
||
After I described 640x400 mode in a magazine article, Bill Lindley, of
|
||
Mesa, Arizona, wrote me to suggest that when programming the VGA to a
|
||
nonstandard mode of this sort, it's a good idea to tell the BIOS about
|
||
the new screen size, for a couple of reasons. For one thing, pop-up
|
||
utilities often use the BIOS variables; Bill's memory-resident screen
|
||
printer, EGAD Screen Print, determines the number of scan lines to print
|
||
by multiplying the BIOS "number of text rows" variable times the
|
||
"character height" variable. For another, the BIOS itself may do a poor
|
||
job of displaying text if not given proper information; the active text
|
||
area may not match the screen dimensions, or an inappropriate graphics
|
||
font may be used. (Of course, the BIOS isn't going to be able to display
|
||
text anyway in highly nonstandard modes such as Mode X, but it will do
|
||
fine in slightly nonstandard modes such as 640x400 16-color mode.) In
|
||
the case of the 640x400 16-color model described a little earlier, Bill
|
||
suggests that the code in Listing 45.4 be called immediately after
|
||
putting the VGA into that mode to tell the BIOS that we're working with
|
||
25 rows of 16-pixel-high text. I think this is an excellent suggestion;
|
||
it can't hurt, and may save you from getting aggravating tech support
|
||
calls down the road.
|
||
|
||
**LISTING 45.4 L45-4.C**
|
||
|
||
```c
|
||
/* Function to tell the BIOS to set up properly sized characters for 25 rows of
|
||
16 pixel high text in 640x400 graphics mode. Call immediately after mode set.
|
||
Based on a contribution by Bill Lindley. */
|
||
|
||
#include <dos.h>
|
||
|
||
void Set640x400()
|
||
{
|
||
union REGS regs;
|
||
|
||
regs.h.ah = 0x11; /* character generator function */
|
||
regs.h.al = 0x24; /* use ROM 8x6 character set for graphics */
|
||
regs.h.bl = 2; /* 25 rows */
|
||
int86(0x10, ®s, ®s); /* invoke the BIOS video interrupt
|
||
to set up the text */
|
||
}
|
||
```
|
||
|
||
The 640x400 mode I've described here isn't exactly earthshaking, but it
|
||
can come in handy for page flipping and CGA emulation, and I'm sure that
|
||
some of you will find it useful at one time or another.
|
||
|
||
### Another Interesting Twist on Page Flipping
|
||
|
||
I've spent a fair amount of time exploring various ways to do animation.
|
||
I thought I had pegged all the possible ways to do animation:
|
||
exclusive-ORing; simply drawing and erasing objects; drawing objects
|
||
with a blank fringe to erase them at their old locations as they're
|
||
drawn; page flipping; and, finally, drawing to local memory and copying
|
||
the dirty (modified) rectangles to the screen, as I've discussed in this
|
||
chapter.
|
||
|
||
To my surprise, someone threw me an interesting and useful twist on
|
||
animation not long ago, which turned out to be a cross between page
|
||
flipping and dirty-rectangle animation. That someone was Serge Mathieu
|
||
of Concepteva Inc., in Rosemere, Quebec, who informed me that he designs
|
||
everything "from a game *point de vue*."
|
||
|
||
In normal page flipping, you display one page while you update the other
|
||
page. Then you display the new page while you update the other. This
|
||
works fine, but the need to keep two pages current can make for a lot of
|
||
bookkeeping and possibly extra drawing, especially in applications where
|
||
only some of the objects are redrawn each time.
|
||
|
||
Serge didn't care to do all that bookkeeping in his animation
|
||
applications, so he came up with the following approach, which I've
|
||
reworded, amplified, and slightly modified in the summary here:
|
||
|
||
1. Set the start address to display page 0.
|
||
|
||
2. Draw to page 1.
|
||
|
||
3. Set the start address to display page 1 (the newly drawn page),
|
||
then wait for the leading edge of vertical sync, at which point the page
|
||
has flipped and it's safe to modify page 0.
|
||
|
||
4. Copy, via the latches, from page 1 to page 0 the areas that
|
||
changed from the previous screen to the current one.
|
||
|
||
5. Set the start address to display page 0, which is now identical
|
||
to page 1, then wait for the leading edge of vertical sync, at which
|
||
point the page has flipped and it's safe to modify page 1.
|
||
|
||
6. Go to step 2.
|
||
|
||
The great benefit of Serge's approach is that the only page that is ever
|
||
actually drawn to (as opposed to being block-copied to) is page 1. Only
|
||
one page needs to be maintained, and the complications of maintaining
|
||
two separate pages vanish entirely. The performance of Serge's approach
|
||
may be better or worse than standard page flipping, depending on whether
|
||
a lot of extra work is required to maintain two pages or not. My guess
|
||
is that Serge's approach will usually be slower, owing to the
|
||
considerable amount of display-memory copying involved, and also to the
|
||
double page-flip per frame. There's no doubt, however, that Serge's
|
||
approach is simpler, and the resultant display quality is every bit as
|
||
good as standard page flipping. Given page flipping's fair degree of
|
||
complication, this approach is a valuable tool, especially for
|
||
less-experienced animation programmers.
|
||
|
||
An interesting variation on Serge's approach doesn't page flip nor wait
|
||
for vertical sync:
|
||
|
||
1. Set the start address to display page 0.
|
||
|
||
2. Draw to page 1.
|
||
|
||
3. Copy, via the latches, the areas that changed from the last
|
||
screen to the current one from page 1 to page 0.
|
||
|
||
4. Go to step 2.
|
||
|
||
This approach totally eliminates page flipping, which can consume a
|
||
great deal of time. The downside is that images may shear for one frame
|
||
if they're only partially copied when the raster beam reaches them. This
|
||
approach is basically a standard dirty-rectangle approach, except that
|
||
the drawing buffer is stored in display memory, rather than in system
|
||
memory. Whether this technique is faster than drawing to system memory
|
||
depends on whether the benefit you get from the VGA's hardware, such as
|
||
the Bit Mask, the ALUs, and especially the latches (for copying the
|
||
dirty rectangles) is sufficient to outweigh the extra display-memory
|
||
accesses involved in drawing and copying, since display memory is
|
||
notoriously slow.
|
||
|
||
Finally, I'd like to point out that in any scheme that involves changing
|
||
the display-memory start address, a clever trick can potentially reduce
|
||
the time spent waiting for pages to flip. Normally, it's necessary to
|
||
wait for display enable to be active, then set the two start address
|
||
registers, and finally wait for vertical sync to be active, so that you
|
||
know the new start address has taken effect. The start-address registers
|
||
must never be set around the time vertical sync is active (the new start
|
||
address is accepted at either the start or end of vertical sync on the
|
||
EGAs and VGAs I'm familiar with), because it would then be possible to
|
||
load a half-changed start address (one register loaded, the other not
|
||
yet loaded), and the screen would jump for a frame. Avoiding this
|
||
condition is the motivation for waiting for display enable, because
|
||
display enable is active only when vertical sync is not active and will
|
||
not become active for a long while.
|
||
|
||
Suppose, however, that you arrange your page start addresses so that
|
||
they both have a low-byte value of 0 (page 0 starts at 0000H, and page 1
|
||
starts at 8000H, for example). Page flipping can then be done simply by
|
||
setting the new high byte of the start address, then waiting for the
|
||
leading edge of vertical sync. This eliminates the need to wait for
|
||
display enable (the two bytes of the start address can never be
|
||
mismatched); page flipping will often involve less waiting, because
|
||
display enable becomes inactive long before vertical sync becomes
|
||
active. Using the above approach reclaims all the time between the end
|
||
of display enable and the start of vertical sync for doing useful work.
|
||
(The steps I've given for Serge's animation approach assume that the
|
||
single-byte approach is in use; that's why display enable is never
|
||
waited for.)
|
||
|
||
In the next chapter, I'll return to the original dirty-rectangle
|
||
algorithm presented in this chapter, and goose it a little with some
|
||
assembly, so that we can see what dirty-rectangle animation is really
|
||
made of. (Probably not dog hair....)
|