abrash-black-book/chapter-34.md
2014-01-06 23:29:31 +11:00

755 lines
38 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: '34'
pages: 637-652
---
## Chapter 34 -- Changing Colors without Writing Pixels
### Special Effects through Realtime Manipulation of DAC Colors
Sometimes, strange as it may seem, the harder you try, the less you
accomplish. Brute force is fine when it suffices, but it does not always
suffice, and when it does not, finesse and alternative approaches are
called for. Such is the case with rapidly cycling through colors by
repeatedly loading the VGA's Digital to Analog Converter (DAC). No
matter how much you optimize your code, you just can't reliably load the
whole DAC cleanly in a single frame, so you had best find other ways to
use the DAC to cycle colors. What's more, BIOS support for DAC loading
is so inconsistent that it's unusable for color cycling; direct loading
through the I/O ports is the only way to go. We'll see why next, as we
explore color cycling, and then finish up this chapter and this section
by cleaning up some odds and ends about VGA color.
There's a lot to be said about loading the DAC, so let's dive right in
and see where the complications lie.
### Color Cycling
As we've learned in past chapters, the VGA's DAC contains 256 storage
locations, each holding one 18-bit value representing an RGB color
triplet organized as 6 bits per primary color. Each and every pixel
generated by the VGA is fed into the DAC as an 8-bit value (refer to
Chapter 33 and to Chapter A on the companion CD-ROM to see how pixels
become 8-bit values in non-256 color modes) and each 8-bit value is used
to look up one of the 256 values stored in the DAC. The looked-up value
is then converted to analog red, green, and blue signals and sent to the
monitor to form one pixel.
That's straightforward enough, and we've produced some pretty impressive
color effects by loading the DAC once and then playing with the 8-bit
path into the DAC. Now, however, we want to generate color effects by
dynamically changing the values stored in the DAC in real time, a
technique that I'll call *color cycling*. The potential of color cycling
should be obvious: Smooth motion can easily be simulated by altering the
colors in an appropriate pattern, and all sorts of changing color
effects can be produced without altering a single bit of display memory.
For example, a sunset can be made to color and darken by altering the
DAC locations containing the colors used to draw the sunset, or a river
can be made to appear to flow by cycling through the colors used to draw
the river. Another use for color cycling is in providing more realistic
displays for applications like realtime 3-D games, where the VGA's 256
simultaneous colors can be made to seem like many more by changing the
DAC settings from frame to frame to match the changing color demands of
the rendered scene. Which leaves only one question: How do we load the
DAC smoothly in realtime?
Actually, so far as I know, you can't. At least you can't load the
*entire* DAC—all 256 locations—frame after frame without producing
distressing on-screen effects on at least some computers. In non-256
color modes, it is indeed possible to load the DAC quickly enough to
cycle all displayed colors (of which there are 16 or fewer), so color
cycling could be used successfully to cycle all colors in such modes. On
the other hand, color paging (which flips among a number of color sets
stored within the DAC in all modes other than 256 color mode, as
discussed in Chapter A on the companion CD-ROM) can be used in non-256
color modes to produce many of the same effects as color cycling and is
considerably simpler and more reliable then color cycling, so color
paging is generally superior to color cycling whenever it's available.
In short, color cycling is really the method of choice for dynamic color
effects only in 256-color mode—but, regrettably, color cycling is at its
least reliable and capable in that mode, as we'll see next.
### The Heart of the Problem
Here's the problem with loading the entire DAC repeatedly: The DAC
contains 256 color storage locations, each loaded via either 3 or 4
`OUT` instructions (more on that next), so at least 768 `OUT`s are
needed to load the entire DAC. That many `OUT`s take a considerable
amount of time, all the more so because `OUT`s are painfully slow on
486s and Pentiums, and because the DAC is frequently on the ISA bus
(although VLB and PCI are increasingly common), where wait states are
inserted in fast computers. In an 8 MHz AT, 768 `OUT`s alone would
take 288 microseconds, and the data loading and looping that are also
required would take in the ballpark of 1,800 microseconds more, for a
minimum of 2 milliseconds total.
As it happens, the DAC should only be loaded during vertical blanking;
that is, the time between the end of displaying the bottom border and
the start of displaying the top border, when no video information at all
is being sent to the screen by the DAC. Otherwise, small dots of snow
appear on the screen, and while an occasional dot of this sort wouldn't
be a problem, the constant DAC loading required by color cycling would
produce a veritable snowstorm on the screen. By the way, I do mean
"border," not "frame buffer"; the overscan pixels pass through the DAC
just like the pixels controlled by the frame buffer, so you can't even
load the DAC while the border color is being displayed without getting
snow.
The start of vertical blanking itself is not easy to find, but the
leading edge of the vertical sync pulse is easy to detect via bit 3 of
the Input Status 1 register at 3DAH; when bit 3 is 1, the vertical sync
pulse is active. Conveniently, the vertical sync pulse starts partway
through but not too far into vertical blanking, so it serves as a handy
way to tell when it's safe to load the DAC without producing snow on the
screen.
So we wait for the start of the vertical sync pulse, then begin to load
the DAC. There's a catch, though. On many computers—Pentiums, 486s, and
386s sometimes, 286s most of the time, and 8088s all the time—there just
isn't enough time between the start of the vertical sync pulse and the
end of vertical blanking to load all 256 DAC locations. That's the crux
of the problem with the DAC, and shortly we'll get to a tool that will
let you explore for yourself the extent of the problem on computers in
which you're interested. First, though, we must address *another* DAC
loading problem: the BIOS.
#### Loading the DAC via the BIOS
The DAC can be loaded either directly or through subfunctions 10H (for a
single DAC register) or 12H (for a block of DAC registers) of the BIOS
video service interrupt 10H, function 10H, described in Chapter 33. For
cycling the contents of the entire DAC, the block-load function (invoked
by executing `INT` 10H with AH = 10H and AL = 12H to load a block of
CX DAC locations, starting at location BX, from the block of RGB
triplets—3 bytes per triplet—starting at ES:DX into the DAC) would be
the better of the two, due to the considerably greater efficiency of
calling the BIOS once rather than 256 times. At any rate, we'd like to
use one or the other of the BIOS functions for color cycling, because we
know that whenever possible, one should use a BIOS function in
preference to accessing hardware directly, in the interests of avoiding
compatibility problems. In the case of color cycling, however, it is
emphatically *not* possible to use either of the BIOS functions, for
they have problems. Serious problems.
The difficulty is this: IBM's BIOS specification describes exactly how
the parameters passed to the BIOS control the loading of DAC locations,
and all clone BIOSes meet that specification scrupulously, which is to
say that if you invoke `INT` 10H, function 10H, subfunction 12H with a
given set of parameters, you can be sure that you will end up with the
same values loaded into the same DAC locations on all VGAs from all
vendors. IBM's spec does *not*, however, describe whether vertical
retrace should be waited for before loading the DAC, nor does it mention
whether video should be left enabled while loading the DAC, leaving
cloners to choose whatever approach they desire—and, alas, every VGA
cloner seems to have selected a different approach.
I tested four clone VGAs from different manufacturers, some in a 20 MHz
386 machine and some in a 10 MHz 286 machine. Two of the four waited for
vertical retrace before loading the DAC; two didn't. Two of the four
blanked the display while loading the DAC, resulting in flickering bars
across the screen. One showed speckled pixels spattered across the top
of the screen while the DAC was being loaded. Also, not one was able to
load all 256 DAC locations without showing *some* sort of garbage on the
screen for at least one frame, but that's not the BIOS's fault; it's a
problem endemic to the VGA.
> ![](images/i.jpg)
> These findings lead me inexorably to the conclusion that the BIOS should
> not be used to load the DAC dynamically. That is, if you're loading the
> DAC just once in preparation for a graphics session—sort of a DAC mode
> set—by all means load by way of the BIOS. No one will care that some
> garbage is displayed for a single frame; heck, I have boards that bounce
> and flicker and show garbage every time I do a mode set, and the amount
> of garbage produced by loading the DAC once is far less noticeable. If,
> however, you intend to load the DAC repeatedly for color cycling, avoid
> the BIOS DAC load functions like the plague. They will bring you only
> heartache.
As but one example of the unsuitability of the BIOS DAC-loading
functions for color cycling, imagine that you want to cycle all 256
colors 70 times a second, which is once per frame. In order to
accomplish that, you would normally wait for the start of the vertical
sync signal (marking the end of the frame), then call the BIOS to load
the DAC. On some boards—boards with BIOSes that don't wait for vertical
sync before loading the DAC—that will work pretty well; you will, in
fact, load the DAC once a frame. On other boards, however, it will work
very poorly indeed; your program will wait for the start of vertical
sync, and then the BIOS will wait for the start of the next vertical
sync, with the result being that the DAC gets loaded only once every
*two* frames. Sadly, there's no way, short of actually profiling the
performance of BIOS DAC loads, for you to know which sort of BIOS is
installed in a particular computer, so unless you can always control the
brand of VGA your software will run on, you really can't afford to color
cycle by calling the BIOS.
Which is not to say that loading the DAC directly is a picnic either, as
we'll see next.
#### Loading the DAC Directly
So we must load the DAC directly in order to perform color cycling. The
DAC is loaded directly by sending (with an `OUT` instruction) the
number of the DAC location to be loaded to the DAC Write Index register
at 3C8H and then performing three `OUT`s to write an RGB triplet to
the DAC Data register at 3C9H. This approach must be repeated 256 times
to load the entire DAC, requiring over a thousand `OUT`s in all.
There is another, somewhat faster approach, but one that has its risks.
After an RGB triplet is written to the DAC Data register, the DAC Write
Index register automatically increments to point to the next DAC
location, and this repeats indefinitely as successive RGB triplets are
written to the DAC. By taking advantage of this feature, the entire DAC
can be loaded with just 769 `OUT`s: one `OUT` to the DAC Write Index
register and 768 `OUT`s to the DAC Data register.
So what's the drawback? Well, imagine that as you're loading the DAC, an
interrupt-driven TSR (such as a program switcher or multitasker)
activates and writes to the DAC; you could end up with quite a mess on
the screen, especially when your program resumes and continues writing
to the DAC—but in all likelihood to the wrong locations. No problem, you
say; just disable interrupts for the duration. Good idea—but it takes
much longer to load the DAC than interrupts should be disabled for. If,
on the other hand, you set the index for each DAC location separately,
you can disable interrupts 256 times, once as each DAC location is
loaded, without problems.
As I commented in the last chapter, I don't have any gruesome tale to
relate that mandates taking the slower but safer road and setting the
index for each DAC location separately while interrupts are disabled.
I'm merely hypothesizing as to what ghastly mishaps *could*. happen.
However, it's been my experience that anything that can happen on the PC
*does* happen eventually; there are just too dang many PCs out there for
it to be otherwise. However, load the DAC any way you like; just don't
blame me if you get a call from someone who's claims that your program
sometimes turns their screen into something resembling month-old yogurt.
It's not really your fault, of course—but try explaining that to *them!*
### A Test Program for Color Cycling
Anyway, the choice of how to load the DAC is yours. Given that I'm not
providing you with any hard-and-fast rules (mainly because there don't
seem to be any), what you need is a tool so that you can experiment with
various DAC-loading approaches for yourself, and that's exactly what
you'll find in Listing 34.1.
Listing 34.1 draws a band of vertical lines, each one pixel wide, across
the screen. The attribute of each vertical line is one greater than that
of the preceding line, so there's a smooth gradient of attributes from
left to right. Once everything is set up, the program starts cycling the
colors stored in however many DAC locations are specified by the
`CYCLE_SIZE` equate; as many as all 256 DAC locations can be cycled.
(Actually, `CYCLE_SIZE`-1 locations are cycled, because location 0 is
kept constant in order to keep the background and border colors from
changing, but `CYCLE_SIZE` locations are *loaded*, and it's the
number of locations we can load without problems that we're interested
in.)
**LISTING 34.1 L34-1.ASM**
```nasm
; Fills a band across the screen with vertical bars in all 256
; attributes, then cycles a portion of the palette until a key is
; pressed.
; Assemble with MASM or TASM
USE_BIOS equ 1 ;set to 1 to use BIOS functions to access the
; DAC, 0 to read and write the DAC directly
GUARD_AGAINST_INTS equ 1 ;1 to turn off interrupts and set write index
; before loading each DAC location, 0 to rely
; on the DAC auto-incrementing
WAIT_VSYNC equ 1 ;set to 1 to wait for the leading edge of
; vertical sync before accessing the DAC, 0
; not to wait
NOT_8088 equ 0 ;set to 1 to use REP INSB and REP OUTSB when
; accessing the DAC directly, 0 to use
; IN/STOSB and LODSB/OUT
CYCLE_SIZE equ 256 ;# of DAC locations to cycle, 256 max
SCREEN_SEGMENT equ 0a000h ;mode 13h display memory segment
SCREEN_WIDTH_IN_BYTES equ 320 ;# of bytes across the screen in mode 13h
INPUT_STATUS_1 equ 03dah ;input status 1 register port
DAC_READ_INDEX equ 03c7h ;DAC Read Index register
DAC_WRITE_INDEX equ 03c8h ;DAC Write Index register
DAC_DATA equ 03c9h ;DAC Data register
if NOT_8088
.286
endif ;NOT_8088
.model small
.stack 100h
.data
;Storage for all 256 DAC locations, organized as one three-byte
; (actually three 6-bit values; upper two bits of each byte aren't
; significant) RGB triplet per color.
PaletteTempdb 256*3 dup(?)
.code
start:
mov ax,@data
mov ds,ax
;Select VGA's standard 256-color graphics mode, mode 13h.
mov ax,0013h ;AH = 0: set mode function,
int 10h ; AL = 13h: mode # to set
;Read all 256 DAC locations into PaletteTemp (3 6-bit values, one
; each for red, green, and blue, per DAC location).
if WAIT_VSYNC
;Wait for the leading edge of the vertical sync pulse; this ensures
; that we read the DAC starting during the vertical non-display
; period.
mov dx,INPUT_STATUS_1
WaitNotVSync: ;wait to be out of vertical sync
in al,dx
and al,08h
jnz WaitNotVSync
WaitVSync: ;wait until vertical sync begins
in al,dx
and al,08h
jz WaitVSync
endif ;WAIT_VSYNC
if USE_BIOS
mov ax,1017h ;AH = 10h: set DAC function,
; AL = 17h: read DAC block subfunction
sub bx,bx ;start with DAC location 0
mov cx,256 ;read out all 256 locations
mov dx,seg PaletteTemp
mov es,dx
mov dx,offset PaletteTemp ;point ES:DX to array in which
; the DAC values are to be stored
int 10h ;read the DAC
else ;!USE_BIOS
if GUARD_AGAINST_INTS
mov cx,CYCLE_SIZE ;# of DAC locations to load
mov di,seg PaletteTemp
mov es,di
mov di,offset PaletteTemp ;dump the DAC into this array
sub ah,ah ;start with DAC location 0
DACStoreLoop:
mov dx,DAC_READ_INDEX
mov al,ah
cli
out dx,al ;set the DAC location #
mov dx,DAC_DATA
in al,dx ;get the red component
stosb
in al,dx ;get the green component
stosb
in al,dx ;get the blue component
stosb
sti
inc ah
loopDACStoreLoop
else;!GUARD_AGAINST_INTS
mov dx,DAC_READ_INDEX
sub al,al
out dx,al ;set the initial DAC location to 0
mov di,seg PaletteTemp
mov es,di
mov di,offset PaletteTemp ;dump the DAC into this array
mov dx,DAC_DATA
if NOT_8088
mov cx,CYCLE_SIZE*3
rep insb ;read CYCLE_SIZE DAC locations at once
else;!NOT_8088
mov cx,CYCLE_SIZE ;# of DAC locations to load
DACStoreLoop:
in al,dx ;get the red component
stosb
in al,dx ;get the green component
stosb
in al,dx ;get the blue component
stosb
loop DACStoreLoop
endif ;NOT_8088
endif ;GUARD_AGAINST_INTS
endif ;USE_BIOS
;Draw a series of 1-pixel-wide vertical bars across the screen in
; attributes 1 through 255.
mov ax,SCREEN_SEGMENT
mov es,ax
mov di,50*SCREEN_WIDTH_IN_BYTES ;point ES:DI to the start
; of line 50 on the screen
cld
mov dx,100 ;draw 100 lines high
RowLoop:
mov al,1 ;start each line with attr 1
mov cx,SCREEN_WIDTH_IN_BYTES ;do a full line across
ColumnLoop:
stosb ;draw a pixel
add al,1 ;increment the attribute
ad cal,0 ;if the attribute just turned
; over to 0, increment it to 1
; because we're not going to
; cycle DAC location 0, so
; attribute 0 won't change
loop ColumnLoop
dec dx
jnz RowLoop
;Cycle the specified range of DAC locations until a key is pressed.
CycleLoop:
;Rotate colors 1-255 one position in the PaletteTemp array;
; location 0 is always left unchanged so that the background
; and border don't change.
push word ptr PaletteTemp+(1*3) ;set aside PaletteTemp
push word ptr PaletteTemp+(1*3)+2; setting for attr 1
mov cx,254
mov si,offset PaletteTemp+(2*3)
mov di,offset PaletteTemp+(1*3)
mov ax,ds
mov es,ax
mov cx,254*3/2
rep movsw ;rotate PaletteTemp settings
; for attrs 2 through 255 to
; attrs 1 through 254
pop bx ;get back original settings
pop ax ; for attribute 1 and move
stosw ; them to the PaletteTemp
mov es:[di],bl ; location for attribute 255
if WAIT_VSYNC
;Wait for the leading edge of the vertical sync pulse; this ensures
; that we reload the DAC starting during the vertical non-display
; period.
mov dx,INPUT_STATUS_1
WaitNotVSync2: ;wait to be out of vertical sync
in al,dx
and al,08h
jnz WaitNotVSync2
WaitVSync2: ;wait until vertical sync begins
in al,dx
and al,08h
jz WaitVSync2
endif ;WAIT_VSYNC
if USE_BIOS
;Set the new, rotated palette.
mov ax,1012h ;AH = 10h: set DAC function,
; AL = 12h: set DAC block subfunction
sub bx,bx ;start with DAC location 0
mov cx,CYCLE_SIZE ;# of DAC locations to set
mov dx,seg PaletteTemp
mov es,dx
mov dx,offset PaletteTemp ;point ES:DX to array from which
; to load the DAC
int 10h ;load the DAC
else ;!USE_BIOS
if GUARD_AGAINST_INTS
mov cx,CYCLE_SIZE ;# of DAC locations to load
mov si,offset PaletteTemp ;load the DAC from this array
sub ah,ah ;start with DAC location 0
DACLoadLoop:
mov dx,DAC_WRITE_INDEX
mov al,ah
cli
out dx,al ;set the DAC location #
mov dx,DAC_DATA
lodsb
out dx,al ;set the red component
lodsb
out dx,al ;set the green component
lodsb
out dx,al ;set the blue component
sti
inc ah
loop DACLoadLoop
else;!GUARD_AGAINST_INTS
mov dx,DAC_WRITE_INDEX
sub al,al
out dx,al ;set the initial DAC location to 0
mov si,offset PaletteTemp ;load the DAC from this array
mov dx,DAC_DATA
if NOT_8088
mov cx,CYCLE_SIZE*3
rep outsb ;load CYCLE_SIZE DAC locations at once
else;!NOT_8088
mov cx,CYCLE_SIZE ;# of DAC locations to load
DACLoadLoop:
lodsb
out dx,al ;set the red component
lodsb
out dx,al ;set the green component
lodsb
out dx,al ;set the blue component
loop DACLoadLoop
endif;NOT_8088
endif;GUARD_AGAINST_INTS
endif;USE_BIOS
;See if a key has been pressed.
mov ah,0bh ;DOS check standard input status fn
int 21h
and al,al ;is a key pending?
jz CycleLoop ;no, cycle some more
;Clear the keypress.
mov ah,1 ;DOS keyboard input fn
int 21h
;Restore text mode and done.
mov ax,0003h ;AH = 0: set mode function,
int 10h ; AL = 03h: mode # to set
mov ah,4ch ;DOS terminate process fn
int 21h
endstart
```
The big question is, How does Listing 34.1 cycle colors? Via the BIOS or
directly? With interrupts enabled or disabled? *Et cetera?*
However you like, actually. Four equates at the top of Listing 34.1
select the sort of color cycling performed; by changing these equates
and `CYCLE_SIZE`, you can get a feel for how well various approaches
to color cycling work with whatever combination of computer system and
VGA you care to test.
The `USE_BIOS` equate is simple. Set `USE_BIOS` to 1 to load the
DAC through the block-load-DAC BIOS function, or to 0 to load the DAC
directly with `OUT`s.
If `USE_BIOS` is 1, the only other equate of interest is
`WAIT_VSYNC`. If `WAIT_VSYNC` is 1, the program waits for the
leading edge of vertical sync before loading the DAC; if `WAIT_VSYNC`
is 0, the program doesn't wait before loading. The effect of setting or
not setting `WAIT_VSYNC` depends on whether the BIOS of the VGA the
program is running on waits for vertical sync before loading the DAC.
You may end up with a double wait, causing color cycling to proceed at
half speed, you may end up with no wait at all, causing cycling to occur
far too rapidly (and almost certainly with hideous on-screen effects),
or you may actually end up cycling at the proper one-cycle-per-frame
rate.
If `USE_BIOS` is 0, `WAIT_VSYNC` still applies. However, you will
always want to set `WAIT_VSYNC` to 1 when `USE_BIOS` is 0;
otherwise, cycling will occur much too fast, and a good deal of
continuous on-screen garbage is likely to make itself evident as the
program loads the DAC non-stop.
If `USE_BIOS` is 0, `GUARD_AGAINST_INTS` determines whether the
possibility of the DAC loading process being interrupted is guarded
against by disabling interrupts and setting the write index once for
every location loaded and whether the DAC's autoincrementing feature is
relied upon or not.
If `GUARD_AGAINST_INTS` is 1, the following sequence is followed for
the loading of each DAC location in turn: Interrupts are disabled, the
DAC Write Index register is set appropriately, the RGB triplet for the
location is written to the DAC Data register, and interrupts are
enabled. This is the slow but safe approach described earlier.
Matters get still more interesting if `GUARD_AGAINST_INTS` is 0. In
that case, if `NOT_8088` is 0, then an autoincrementing load is
performed in a straightforward fashion; the DAC Write Index register is
set to the index of the first location to load and the RGB triplet is
sent to the DAC by way of three `LODSB/OUT DX,AL` pairs, with `LOOP`
repeating the process for each of the locations in turn.
If, however, `NOT_8088` is 1, indicating that the processor is a 286
or better (perhaps `AT_LEAST_286` would have been a better name),
then after the initial DAC Write Index value is set, all 768 DAC
locations are loaded with a single `REP OUTSB`. This is clearly the
fastest approach, but it runs the risk, albeit remote, that the loading
sequence will be interrupted and the DAC registers will become garbled.
My own experience with Listing 34.1 indicates that it is sometimes
possible to load all 256 locations cleanly but sometimes it is not; it
all depends on the processor, the bus speed, the VGA, and the DAC, as
well as whether autoincrementation and `REP OUTSB` are used. I'm not
going to bother to report how many DAC locations I *could* successfully
load with each of the various approaches, for the simple reason that I
don't have enough data points to make reliable suggestions, and I don't
want you acting on my comments and running into trouble down the pike.
You now have a versatile tool with which to probe the limitations of
various DAC-loading approaches; use it to perform your own tests on a
sampling of the slowest hardware configurations you expect your programs
to run on, then leave a generous safety margin.
One thing's for sure, though—you're not going to be able to cycle all
256 DAC locations cleanly once per frame on a reliable basis across the
current generation of PCs. That's why I said at the outset that brute
force isn't appropriate to the task of color cycling. That doesn't mean
that color cycling can't be used, just that subtler approaches must be
employed. Let's look at some of those alternatives.
### Color Cycling Approaches that Work
First of all, I'd like to point out that when color cycling does work,
it's a thing of beauty. Assemble Listing 34.1 so that it doesn't use the
BIOS to load the DAC, doesn't guard against interrupts, and uses
286-specific instructions if your computer supports them. Then tinker
with `CYCLE_SIZE` until the color cycling is perfectly clean on your
computer. Color cycling looks stunningly smooth, doesn't it? And this is
crude color cycling, working with the default color set; switch over to
a color set that gradually works its way through various hues and
saturations, and you could get something that looks for all the world
like true-color animation (albeit working with a small subset of the
full spectrum at any one time).
Given that, how can we take advantage of color cycling within the
limitations of loading the DAC? The simplest approach, and my personal
favorite, is that of cycling a portion of the DAC while using the rest
of the DAC locations for other, non-cycling purposes. For example, you
might allocate 32 DAC locations to the aforementioned sunset, reserve
160 additional locations for use in drawing a static mountain scene, and
employ the remaining 64 locations to draw images of planes, cars, and
the like in the foreground. The 32 sunset colors could be cycled
cleanly, and the other 224 colors would remain the same throughout the
program, or would change only occasionally.
That suggests a second possibility: If you have several different color
sets to be cycled, interleave the loading so that only one color set is
cycled per frame. Suppose you are animating a night scene, with stars
twinkling in the background, meteors streaking across the sky, and a
spaceship moving across the screen with its jets flaring. One way to
produce most of the necessary effects with little effort would be to
draw the stars in several attributes and then cycle the colors for
*those* attributes, draw the meteor paths in successive attributes, one
for each pixel, and then cycle the colors for those attributes, and do
much the same for the jets. The only remaining task would be to animate
the spaceship across the screen, which is not a particularly difficult
task.
> ![](images/i.jpg)
> The key to getting all the color cycling to work in the above example,
> however, would be to assign each color cycling task a different part of
> the DAC, with each part cycled independently as needed. If, as is
> likely, the total number of DAC locations cycled proved to be too great
> to manage in one frame, you could simply cycle the colors of the stars
> after one frame, the colors of the meteors after the next, and the
> colors of the jets after yet another frame, then back around to cycling
> the colors of the stars. By splitting up the DAC in this manner and
> interleaving the cycling tasks, you can perform a great deal of
> seemingly complex color animation without loading very much of the DAC
> during any one frame.
Yet another and somewhat odder workaround is that of using only 128 DAC
locations and page flipping. (Page flipping in 256-color modes involves
using the VGA's undocumented 256-color modes; see Chapters 31, 43, and
47 for details.) In this mode of operation, you'd first display page 0,
which is drawn entirely with colors 0-127. Then you'd draw page 1 to
look just like page 0, except that colors 128-255 are used instead.
You'd load DAC locations 128-255 with the next cycle settings for the
128 colors you're using, then you'd switch to display the second page
with the new colors. Then you could modify page 0 as needed, drawing in
colors 0-127, load DAC locations 0-127 with the next color cycle
settings, and flip back to page 0.
The idea is that you modify only those DAC locations that are not used
to display any pixels on the current screen. The advantage of this is
*not*, as you might think, that you don't generate garbage on the screen
when modifying undisplayed DAC locations; in fact, you do, for a spot of
interference will show up if you set a DAC location, displayed or not,
during display time. No, you still have to wait for vertical sync and
load only during vertical blanking before loading the DAC when page
flipping with 128 colors; the advantage is that since none of the DAC
locations you're modifying is currently displayed, you can spread the
loading out over two or more vertical blanking periods—however long it
takes. If you did this without the 128-color page flipping, you might
get odd on-screen effects as some of the colors changed after one frame,
some after the next, and so on—or you might not; changing the entire DAC
in chunks over several frames is another possibility worth considering.
Yet another approach to color cycling is that of loading a bit of the
DAC during each horizontal blanking period. Combine that with counting
scan lines, and you could vastly expand the number of simultaneous
on-screen colors by cycling colors *as a frame is displayed*, so that
the color set changes from scan line to scan line down the screen.
The possibilities are endless. However, were I to be writing 256-color
software that used color cycling, I'd find out how many colors could be
cycled after the start of vertical sync on the slowest computer I
expected the software to run on, I'd lop off at least 10 percent for a
safety margin, and I'd structure my program so that no color cycling set
exceeded that size, interleaving several color cycling sets if
necessary.
That's what *I'd* do. Don't let yourself be held back by my limited
imagination, though! Color cycling may be the most complicated of all
the color control techniques, but it's also the most powerful.
### Odds and Ends
In my experience, when relying on the autoincrementing feature while
loading the DAC, the Write Index register wraps back from 255 to 0, and
likewise when you load a block of registers through the BIOS. So far as
I know, this is a characteristic of the hardware, and should be
consistent; also, Richard Wilton documents this behavior for the BIOS in
the VGA bible, *Programmer's Guide to PC Video Systems, Second Edition*
(Microsoft Press), so you should be able to count on it. Not that I see
that DAC index wrapping is especially useful, but it never hurts to
understand exactly how your resources behave, and I never know when one
of you might come up with a serviceable application for any particular
quirk.
#### The DAC Mask
There's one register in the DAC that I haven't mentioned yet, the DAC
Mask register at 03C6H. The operation of this register is simple but
powerful; it can mask off any or all of the 8 bits of pixel information
coming into the DAC from the VGA. Whenever a bit of the DAC Mask
register is 1, the corresponding bit of pixel information is passed
along to the DAC to be used in looking up the RGB triplet to be sent to
the screen. Whenever a bit of the DAC Mask register is 0, the
corresponding pixel bit is ignored, and a 0 is used for that bit
position in all look-ups of RGB triplets. At the extreme, a DAC Mask
setting of 0 causes all 8 bits of pixel information to be ignored, so
DAC location 0 is looked up for every pixel, and the entire screen
displays the color stored in DAC location 0. This makes setting the DAC
Mask register to 0 a quick and easy way to blank the screen.
#### Reading the DAC
The DAC can be read directly, via the DAC Read Index register at 3C7H
and the DAC Data register at 3C9H, in much the same way as it can be
written directly by way of the DAC Write Index register—complete with
autoincrementing the DAC Read Index register after every three reads.
Everything I've said about writing to the DAC applies to reading from
the DAC. In fact, reading from the DAC can even cause snow, just as
loading the DAC does, so it should ideally be performed during vertical
blanking.
The DAC can also be read by way of the BIOS in either of two ways.
`INT` 10H, function 10H (AH=10H), subfunction 15H (AL=15H) reads out a
single DAC location, specified by BX; this function returns the RGB
triplet stored in the specified location with the red component in the
lower 6 bits of DH, the green component in the lower 6 bits of CH, and
the blue component in the lower 6 bits of CL.
`INT` 10H, function 10H (AH=10H), subfunction 17H (AL=17H) reads out a
block of DAC locations of length CX, starting with the location
specified by BX. ES:DX must point to the buffer in which the RGB values
from the specified block of DAC locations are to be stored. The form of
this buffer (RGB, RGB, RGB ..., with three bytes per RGB triple) is
exactly the same as that of the buffer used when calling the BIOS to
load a block of registers.
Listing 34.1 illustrates reading the DAC both through the BIOS
block-read function and directly, with the direct-read code capable of
conditionally assembling to either guard against interrupts or not and
to use `REP INSB` or not. As you can see, reading the DAC settings is
very much symmetric with setting the DAC.
#### Cycling Down
And so, at long last, we come to the end of our discussion of color
control on the VGA. If it has been more complex than anyone might have
imagined, it has also been most rewarding. There's as much obscure but
very real potential in color control as there is anywhere on the VGA,
which is to say that there's a very great deal of potential indeed. Put
color cycling or color paging together with the page flipping and image
drawing techniques explored elsewhere in this book, and you'll leave the
audience gasping and wondering "How the heck did they *do* that?"