diff --git a/01-01.html b/01-01.html index 7a32bb0..b6efbe1 100644 --- a/01-01.html +++ b/01-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Part I

Chapter 1
@@ -69,7 +72,7 @@

“What’s a fast slow program?” you ask. That’s a good question, and a brief (true) story is perhaps the best answer.

-

When Fast Isn’t Fast

+

When Fast Isn’t Fast

In the early 1970s, as the first hand-held calculators were hitting the market, I knew a fellow named Irwin. He was a good student, and was planning to be an engineer. Being an engineer back then meant knowing how to use a slide rule, and Irwin could jockey a slipstick with the best of them. In fact, he was so good that he challenged a fellow with a calculator to a duel—and won, becoming a local legend in the process.

@@ -77,16 +80,20 @@

What does all this have to do with programming? Plenty. When you spend time optimizing poorly-designed assembly code, or when you count on an optimizing compiler to make your code fast, you’re wasting the optimization, much as Irwin did. Particularly in assembly, you’ll find that without proper up-front design and everything else that goes into high-performance design, you’ll waste considerable effort and time on making an inherently slow program as fast as possible—which is still slow—when you could easily have improved performance a great deal more with just a little thought. As we’ll see, handcrafted assembly language and optimizing compilers matter, but less than you might think, in the grand scheme of things—and they scarcely matter at all unless they’re used in the context of a good design and a thorough understanding of both the task at hand and the PC.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/01-02.html b/01-02.html index 37ca79a..b98bc11 100644 --- a/01-02.html +++ b/01-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Rules for Building High-Performance Code

We’ve got the following rules for creating high-performance software:

@@ -52,15 +55,15 @@

Making rules is easy; the hard part is figuring out how to apply them in the real world. For my money, examining some actual working code is always a good way to get a handle on programming concepts, so let’s look at some of the performance rules in action.

-

Know Where You’re Going

+

Know Where You’re Going

If we’re going to create high-performance code, first we have to know what that code is going to do. As an example, let’s write a program that generates a 16-bit checksum of the bytes in a file. In other words, the program will add each byte in a specified file in turn into a 16-bit value. This checksum value might be used to make sure that a file hasn’t been corrupted, as might occur during transmission over a modem or if a Trojan horse virus rears its ugly head. We’re not going to do anything with the checksum value other than print it out, however; right now we’re only interested in generating that checksum value as rapidly as possible.

-

Make a Big Map

+

Make a Big Map

How are we going to generate a checksum value for a specified file? The logical approach is to get the file name, open the file, read the bytes out of the file, add them together, and print the result. Most of those actions are straightforward; the only tricky part lies in reading the bytes and adding them together.

-

Make Lots of Little Maps

+

Make Lots of Little Maps

Actually, we’re only going to make one little map, because we only have one program section that requires much thought—the section that reads the bytes and adds them up. What’s the best way to do this?

@@ -128,16 +131,20 @@ main(int argc, char *argv[]) {

These results make it clear that it’s folly to rely on your compiler’s optimization to make your programs fast. Listing 1.1 is simply poorly designed, and no amount of compiler optimization will compensate for that failing. To drive home the point, conListings 1.2 and 1.3, which together are equivalent to Listing 1.1 except that the entire checksum loop is written in tight assembly code. The assembly language implementation is indeed faster than any of the C versions, as shown in Table 1.1, but it’s less than 10 percent faster, and it’s still unacceptably slow.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/01-03.html b/01-03.html index f03fb50..f2e64f9 100644 --- a/01-03.html +++ b/01-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-
@@ -266,7 +269,7 @@ _ChecksumFileendp

Well, then, how are we going to improve our design? Before we can do that, we have to understand what’s wrong with the current design.

-

Know the Territory

+

Know the Territory

Just why is Listing 1.1 so slow? In a word: overhead. The C library implements the read() function by calling DOS to read the desired number of bytes. (I figured this out by watching the code execute with a debugger, but you can buy library source code from both Microsoft and Borland.) That means that Listing 1.1 (and Listing 1.3 as well) executes one DOS function per byte processed—and DOS functions, especially this one, come with a lot of overhead.

@@ -278,16 +281,20 @@ _ChecksumFileendp

Listing 1.4 is similar to Listing 1.1, but uses fopen() and getc() (rather than open() and read()) to access the file being checksummed. The results confirm our theories splendidly, and validate our new design. As shown in Table 1.1, Listing 1.4 runs more than an order of magnitude faster than even the assembly version of Listing 1.1, even though Listing 1.1 and Listing 1.4 look almost the same. To the casual observer, read() and getc() would seem slightly different but pretty much interchangeable, and yet in this application the performance difference between the two is about the same as that between a 4.77 MHz PC and a 16 MHz 386.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/01-04.html b/01-04.html index 5807c2a..0cddd0a 100644 --- a/01-04.html +++ b/01-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- @@ -77,7 +80,7 @@ main(int argc, char *argv[]) { } -

Know When It Matters

+

Know When It Matters

The last section contained a particularly interesting phrase: the time-critical portions of your code. Time-critical portions of your code are those portions in which the speed of the code makes a significant difference in the overall performance of your program—and by “significant,” I don’t mean that it makes the code 100 percent faster, or 200 percent, or any particular amount at all, but rather that it makes the program more responsive and/or usable from the user’s perspective.

@@ -93,7 +96,7 @@ main(int argc, char *argv[]) {

Besides, we don’t want to optimize until the design is refined to our satisfaction, and that won’t be the case until we’ve thought about other approaches.

-

Always Consider the Alternatives

+

Always Consider the Alternatives

Listing 1.4 is good, but let’s see if there are other—perhaps less obvious—ways to get the same results faster. Let’s start by considering why Listing 1.4 is so much better than Listing 1.1. Like read(), getc() calls DOS to read from the file; the speed improvement of Listing 1.4 over Listing 1.1 occurs because getc() eads many bytes at once via DOS, then manages those bytes for us. That’s faster than reading them one at a time using read()—but there’s no reason to think that it’s faster than having our program read and manage blocks itself. Easier, yes, but not faster.

@@ -115,16 +118,20 @@ main(int argc, char *argv[]) {

The second reason is the hallmark of the mediocre programmer. Know when optimization matters—and then optimize when it does!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/01-05.html b/01-05.html index c127789..68e860e 100644 --- a/01-05.html +++ b/01-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The third reason is often fallacious. C library functions are not always written in assembly, nor are they always particularly well-optimized. (In fact, they’re often written for portability, which has nothing to do with optimization.) What’s more, they’re general-purpose functions, and often can be outperformed by well-but-not- brilliantly-written code that is well-matched to a specific task. As an example, consider Listing 1.5, which uses internal buffering to handle blocks of bytes at a time. Table 1.1 shows that Listing 1.5 is 2.5 to 4 times faster than Listing 1.4 (and as much as 49 times faster than Listing 1.1!), even though it uses no assembly at all.

@@ -106,7 +109,7 @@ main(int argc, char *argv[]) {

At any rate, Listing 1.5 isn’t much more complicated than Listing 1.4—and it’s a lot faster. Always consider the alternatives; a bit of clever thinking and program redesign can go a long way.

-

Know How to Turn On the Juice

+

Know How to Turn On the Juice

I have said time and again that optimization is pointless until the design is settled. When that time comes, however, optimization can indeed make a significant difference. Table 1.1 indicates that the optimized version of Listing 1.5 produced by Microsoft C outperforms an unoptimized version of the same code by more than 60 percent. What’s more, a mostly-assembly version of Listing 1.5, shown in Listings 1.6 and 1.7, outperforms even the best-optimized C version of List1.5 by 26 percent. These are considerable improvements, well worth pursuing—once the design has been maxed out.

@@ -167,16 +170,20 @@ main(int argc, char *argv[]) { } -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/01-06.html b/01-06.html index 614c06f..b6b5ba7 100644 --- a/01-06.html +++ b/01-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 1.7 L1-7.ASM

 ; Assembler subroutine to perform a 16-bit checksum on a block of
@@ -103,22 +106,26 @@ _ChecksumChunkendp
 
   

Optimization only matters after you’ve done your part on the program design end. Consider the ratios on the vertical axis of Table 1.1, which show that optimization is almost totally wasted in the checksumming application without an efficient design. Optimization is no panacea. Table 1.1 shows a two-times improvement from optimization—and a 50-times-plus improvement from redesign. The longstanding debate about which C compiler optimizes code best doesn’t matter quite so much in light of Table 1.1, does it? Your organic optimizer matters much more than your compiler’s optimizer, and there’s always assembly for those usually small sections of code where performance really matters.

-

Where We’re Going

+

Where We’re Going

This chapter has presented a quick step-by-step overview of the design process. I’m not claiming that this is the only way to create high-performance code; it’s just an approach that works for me. Create code however you want, but never forget that design matters more than detailed optimization. Never stop looking for inventive ways to boost performance—and never waste time speeding up code that doesn’t need to be sped up.

I’m going to focus on specific ways to create high-performance code from now on. In Chapter 5, we’ll continue to look at restartable blocks and internal buffering, in the form of a program that searches files for text strings.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/02-01.html b/02-01.html index 9807c63..01665b0 100644 --- a/02-01.html +++ b/02-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 2
A World Apart

@@ -88,16 +91,20 @@ LoopTop:

To understand why this is so, consider how a program gets written. A programmer examines the requirements of an application, designs a solution at some level of abstraction, and then makes that design come alive in a code implementation. If not handled properly, the transformation that takes place between conception and implementation can reduce performance tremendously; for example, a programmer who implements a routine to search a list of 100,000 sorted items with a linear rather than binary search will end up with a disappointingly slow program.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/02-02.html b/02-02.html index 0cf5f7d..3c315bb 100644 --- a/02-02.html +++ b/02-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Transformation Inefficiencies

+

Transformation Inefficiencies

No matter how well an implementation is derived from the corresponding design, however, high-level languages like C/C++ and Pascal inevitably introduce additional transformation inefficiencies, as shown in Figure 2.1.

@@ -50,11 +53,11 @@

The key, of course, is the programmer, since in assembly the programmer must essentially perform the transformation from the application specification to machine language entirely on his or her own. (The assembler merely handles the direct translation from assembly to machine language.)

-

Self-Reliance

+

Self-Reliance

The first part of assembly language optimization, then, is self. An assembler is nothing more than a tool to let you design machine-language programs without having to think in hexadecimal codes. So assembly language programmers—unlike all other programmers—must take full responsibility for the quality of their code. Since assemblers provide little help at any level higher than the generation of machine language, the assembly programmer must be capable both of coding any programming construct directly and of controlling the PC at the lowest practical level—the operating system, the BIOS, even the hardware where necessary. High-level languages handle most of this transparently to the programmer, but in assembly everything is fair—and necessary—game, which brings us to another aspect of assembly optimization: knowledge.

-

Knowledge

+

Knowledge

In the PC world, you can never have enough knowledge, and every item you add to your store will make your programs better. Thorough familiarity with both the operating system APIs and BIOS interfaces is important; since those interfaces are well-documented and reasonably straightforward, my advice is to get a good book or two and bring yourself up to speed. Similarly, familiarity with the PC hardware is required. While that topic covers a lot of ground—display adapters, keyboards, serial ports, printer ports, timer and DMA channels, memory organization, and more—most of the hardware is well-documented, and articles about programming major hardware components appear frequently in the literature, so this sort of knowledge can be acquired readily enough.

@@ -68,16 +71,20 @@
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/02-03.html b/02-03.html index 65eeb90..1abdf7c 100644 --- a/02-03.html +++ b/02-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The Flexible Mind

Is the never-ending collection of information all there is to the assembly optimization, then? Hardly. Knowledge is simply a necessary base on which to build. Let’s take a moment to examine the objectives of good assembly programming, and the remainder of the forces that act on assembly optimization will fall into place.

@@ -58,20 +61,24 @@

The gist of all this is simply that good assembly programming is done in the context of a solid overall framework unique to each program, and the flexible mind is the key to creating that framework and holding it together.

-

Where to Begin?

+

Where to Begin?

To summarize, the skill of assembly language optimization is a combination of knowledge, perspective, and a way of thought that makes possible the genesis of absolutely the fastest or the smallest code. With that in mind, what should the first step be? Development of the flexible mind is an obvious step. Still, the flexible mind is no better than the knowledge at its disposal. The first step in the journey toward mastering optimization at that exalted level, then, would seem to be learning how to learn.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-01.html b/03-01.html index cd68bce..a68fd29 100644 --- a/03-01.html +++ b/03-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 3
Assume Nothing

@@ -69,16 +72,20 @@

Listing 3.1 shows 8253-based timer software, consisting of three subroutines: ZTimerOn, ZTimerOff, and ZTimerReport. For the remainder of this book, I’ll refer to these routines collectively as the “Zen timer.” C-callable versions of the two precision Zen timers are presented in Chapter K on the companion CD-ROM.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-02.html b/03-02.html index 85baa5b..085343e 100644 --- a/03-02.html +++ b/03-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 3.1 PZTIMER.ASM

 ; The precision Zen timer (PZTIMER.ASM)
@@ -473,16 +476,20 @@ Code   ends
        end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-03.html b/03-03.html index 17e2445..03198ba 100644 --- a/03-03.html +++ b/03-03.html @@ -1,5 +1,4 @@ - + @@ -19,24 +18,28 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

The Zen Timer Is a Means, Not an End

+

The Zen Timer Is a Means, Not an End

We’re going to spend the rest of this chapter seeing what the Zen timer can do, examining how it works, and learning how to use it. I’ll be using the Zen timer again and again over the course of this book, so it’s essential that you learn what the Zen timer can do and how to use it. On the other hand, it is by no means essential that you understand exactly how the Zen timer works. (Interesting, yes; essential, no.)

In other words, the Zen timer isn’t really part of the knowledge we seek; rather, it’s one tool with which we’ll acquire that knowledge. Consequently, you shouldn’t worry if you don’t fully grasp the inner workings of the Zen timer. Instead, focus on learning how to use it, and you’ll be on the right road.

-

Starting the Zen Timer

+

Starting the Zen Timer

ZTimerOn is called at the start of a segment of code to be timed. ZTimerOn saves the context of the calling code, disables interrupts, sets timer 0 of the 8253 to mode 2 (divide-by-N mode), sets the initial timer count to 0, restores the context of the calling code, and returns. (I’d like to note that while Intel’s documentation for the 8253 seems to indicate that a timer won’t reset to 0 until it finishes counting down, in actual practice, timers seem to reset to 0 as soon as they’re loaded.)

@@ -67,16 +70,20 @@

Why not use timer 2 instead of timer 0 for precision timing? After all, timer 2 has a programmable gate input and isn’t used for anything but sound generation. The problem with timer 2 is that its output can’t generate an interrupt; in fact, timer 2 can’t do anything but drive the speaker. We need the interrupt generated by the output of timer 0 to tell us when the count has overflowed, and we will see shortly that the timer interrupt also makes it possible to time much longer periods than the Zen timer shown in Listing 3.1 supports.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-04.html b/03-04.html index feb39cf..aab7b93 100644 --- a/03-04.html +++ b/03-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

In fact, the Zen timer shown in Listing 3.1 can only time intervals of up to about 54 ms in length, since that is the period of time that can be measured by timer 0 before its count turns over and repeats. fifty-four ms may not seem like a very long time, but even a CPU as slow as the 8088 can perform more than 1,000 divides in 54 ms, and division is the single instruction that the 8088 performs most slowly. If a measured period turns out to be longer than 54 ms (that is, if timer 0 has counted down and turned over), the Zen timer will display a message to that effect. A long-period Zen timer for use in such cases will be presented later in this chapter.

The Zen timer determines whether timer 0 has turned over by checking to see whether an IRQ0 interrupt is pending. (Remember, interrupts are off while the Zen timer runs, so the timer interrupt cannot be recognized until the Zen timer stops and enables interrupts.) If an IRQ0 interrupt is pending, then timer 0 has turned over and generated a timer interrupt. Recall that ZTimerOn initially sets timer 0 to 0, in order to allow for the longest possible period—about 54 ms—before timer 0 reaches 0 and generates the timer interrupt.

@@ -70,16 +73,20 @@

You may well want to devise still other approaches better suited to your needs than those I’ve presented. Go to it! I’ve just thrown out a few possibilities to get you started.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-05.html b/03-05.html index f6402d8..8f3d908 100644 --- a/03-05.html +++ b/03-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Notes on the Zen Timer

The Zen timer subroutines are designed to be near-called from assembly language code running in the public segment Code. The Zen timer subroutines can, however, be called from any assembly or high-level language code that generates OBJ files that are compatible with the Microsoft linker, simply by modifying the segment that the timer code runs in to match the segment used by the code being timed, or by changing the Zen timer routines to far procedures and making far calls to the Zen timer code from the code being timed, as discussed at the end of this chapter. All three subroutines preserve all registers and all flags except the interrupt flag, so calls to these routines are transparent to the calling code.

@@ -121,16 +124,20 @@ Skip:

It’s worth noting that Listing 3.3 begins by jumping around the memory variable MemVar. This approach lets us avoid reproducing Listing 3.2 in its entirety for each code fragment we want to measure; by defining any needed data right in the code segment and jumping around that data, each listing becomes self-contained and can be plugged directly into Listing 3.2 as TESTCODE. Listing 3.2 sets DS equal to CS before doing anything else precisely so that data can be embedded in code fragments being timed. Note that only after the initial jump is performed in Listing 3.3 is the Zen timer started, since we don’t want to include the execution time of start-up code in the timing interval. That’s why the calls to ZTimerOn and ZTimerOff are in TESTCODE, not in PZTEST.ASM; this way, we have full control over which portion of TESTCODE is timed, and we can keep set-up code and the like out of the timing interval.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-06.html b/03-06.html index 396ecaf..579e28a 100644 --- a/03-06.html +++ b/03-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 3.3 is used by naming it TESTCODE, assembling both Listing 3.2 (which includes TESTCODE) and Listing 3.1 with TASM or MASM, and linking the two resulting OBJ files together by way of the Borland orMicrosoft linker. Listing 3.4 shows a batch file, PZTIME.BAT, which does all that; when run, this batch file generates and runs the executable file PZTEST.EXE. PZTIME.BAT (Listing 3.4) assumes that the file PZTIMER.ASM contains Listing 3.1, and the file PZTEST.ASM contains Listing 3.2. The command-line parameter to PZTIME.BAT is the name of the file to be copied to TESTCODE and included into PZTEST.ASM. (Note that Turbo Assembler can be substituted for MASM by replacing “masm” with “tasm” and “link” with “tlink” in Listing 3.4. The same is true of Listing 3.7.)

LISTING 3.4 PZTIME.BAT

@@ -123,16 +126,20 @@ pztime <filename>

You should not use the long-period Zen timer to time code that requires interrupts to be disabled for more than 54 ms at a stretch during the timing interval, since when interrupts are disabled the long-period Zen timer is subject to the same 54 ms maximum measurement time as the precision Zen timer.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-07.html b/03-07.html index e4cbaf3..8c62eaf 100644 --- a/03-07.html +++ b/03-07.html @@ -1,5 +1,4 @@ - + @@ -19,22 +18,26 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

While permitting the timer interrupt to occur allows long intervals to be timed, that same interrupt makes the long-period Zen timer less accurate than the precision Zen timer, since the time the BIOS spends handling timer interrupts during the timing interval is included in the time measured by the long-period timer. Likewise, any other interrupts that occur during the timing interval, most notably keyboard and mouse interrupts, will increase the measured time.

The long-period Zen timer has some of the same effects on the system time as does the precision Zen timer, so it’s a good idea to reboot the system after a session with the long-period Zen timer. The long-period Zen timer does not, however, have the same potential for introducing major inaccuracy into the system clock time during a single timing run since it leaves interrupts enabled and therefore allows the system clock to update normally.

-

Stopping the Clock

+

Stopping the Clock

There’s a potential problem with the long-period Zen timer. The problem is this: In order to measure times longer than 54 ms, we must maintain not one but two timing components, the timer 0 count and the BIOS time-of-day count. The time-of-day count measures the passage of 54.9 ms intervals, while the timer 0 count measures time within those 54.9 ms intervals. We need to read the two time components simultaneously in order to get a clean reading. Otherwise, we may read the timer count just before it turns over and generates an interrupt, then read the BIOS time-of-day count just after the interrupt has occurred and caused the time-of-day count to turn over, with a resulting 54 ms measurement inaccuracy. (The opposite sequence—reading the time-of-day count and then the timer count—can result in a 54 ms inaccuracy in the other direction.)

@@ -684,16 +687,20 @@ Code ends end -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-08.html b/03-08.html index 9f42ba1..d80c3af 100644 --- a/03-08.html +++ b/03-08.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Moreover, because it uses an undocumented feature, the timer-stop approach could conceivably cause erratic 8253 operation, which could in turn seriously affect your computer’s operation until the next reboot. In non-8253-compatible systems, I’ve observed not only wildly incorrect timing results, but also failure of a diskette drive to operate properly after the long-period Zen timer with PS2 set to 0 has run, so be alert for signs of trouble if you do set PS2 to 0.

Rebooting should clear up any timer-related problems of the sort described above. (This gives us another reason to reboot at the end of each code-timing session.) You should immediately reboot and set the PS2 equate to 1 if you get erratic or obviously incorrect results with the long-period Zen timer when PS2 is set to 0. If you want to set PS2 to 0, it would be a good idea to time a few of the listings in this book with PS2 set first to 1 and then to 0, to make sure that the results match. If they’re consistently different, you should set PS2 to 1.

@@ -110,16 +113,20 @@ Code ends

As with the precision Zen timer, the program in Listing 3.6 is used by naming the file containing the code to be timed TESTCODE, then assembling both Listing 3.6 and Listing 3.5 with MASM or TASM and linking the two files together by way of the Microsoft or Borland linker. Listing 3.7 shows a batch file, named LZTIME.BAT, which does all of the above, generating and running the executable file LZTEST.EXE. LZTIME.BAT assumes that the file LZTIMER.ASM contains Listing 3.5 and the file LZTEST.ASM contains Listing 3.6.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-09.html b/03-09.html index 5b1626d..a51607c 100644 --- a/03-09.html +++ b/03-09.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 3.7 LZTIME.BAT

 echo off
@@ -146,16 +149,20 @@ lztime lst3-8.asm
 extern “C” ZTimerOn(void);
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/03-10.html b/03-10.html index 854f663..2ba3078 100644 --- a/03-10.html +++ b/03-10.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

when declaring the timer routines extern, so that name-mangling doesn’t occur, and the linker can find the routines’ C-style names.)

That’s all it takes; after doing this, you’ll be able to use the Zen timer from C, as, for example, in:

@@ -50,7 +53,7 @@ ZTimerReport();

The full listings for the C-callable Zen timers are presented in Chapter K on the companion CD-ROM.

-

Watch Out for Optimizing Assemblers!

+

Watch Out for Optimizing Assemblers!

One important safety tip when modifying the Zen timer for use with large code model C code: Watch out for optimizing assemblers! TASM actually replaces

@@ -76,28 +79,32 @@ call     near ptr ReferenceZTimerOn
 
   

I’ve tested the changes shown in Figures 3.2 and 3.3 with TASM and Borland C++ 4.0, and also with the latest MASM and Microsoft C/C++ compiler.

-

Further Reading

+

Further Reading

For those of you who wish to pursue the mechanics of code measurement further, one good article about measuring code performance with the 8253 timer is “Programming Insight: High-Performance Software Analysis on the IBM PC,” by Byron Sheppard, which appeared in the January, 1987 issue of Byte. For complete if somewhat cryptic information on the 8253 timer itself, I refer you to Intel’s Microsystem Components Handbook, which is also a useful reference for a number of other PC components, including the 8259 Programmable Interrupt Controller and the 8237 DMA Controller. For details about the way the 8253 is used in the PC, as well as a great deal of additional information about the PC’s hardware and BIOS resources, I suggest you consult IBM’s series of technical reference manuals for the PC, XT, AT, Model 30, and microchannel computers, such as the Models 50, 60, and 80.

For our purposes, however, it’s not critical that you understand exactly how the Zen timer works. All you really need to know is what the Zen timer can do and how to use it, and we’ve accomplished that in this chapter.

-

Armed with the Zen Timer, Onward and Upward

+

Armed with the Zen Timer, Onward and Upward

The Zen timer is not perfect. For one thing, the finest resolution to which it can measure an interval is at best about 1µs, a period of time in which a 66 MHz Pentium computer can execute as many as 132 instructions (although an 8088-based PC would be hard-pressed to manage two instructions in a microsecond). Another problem is that the timing code itself interferes with the state of the prefetch queue and processor cache at the start of the code being timed, because the timing code is not necessarily fetched and does not necessarily access memory in exactly the same time sequence as the code immediately preceding the code under measurement normally does. This prefetch effect can introduce as much as 3 to 4 µ of inaccuracy. Similarly, the state of the prefetch queue at the end of the code being timed affects how long the code that stops the timer takes to execute. Consequently, the Zen timer tends to be more accurate for longer code sequences, since the relative magnitude of the inaccuracy introduced by the Zen timer becomes less over longer periods.

Imperfections notwithstanding, the Zen timer is a good tool for exploring C code and x86 family assembly language, and it’s a tool we’ll use frequently for the remainder of this book.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-01.html b/04-01.html index 23a9aa2..c5cf83d 100644 --- a/04-01.html +++ b/04-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 4
In the Lair of the Cycle-Eaters

@@ -57,7 +60,7 @@

The nature and severity of the cycle-eaters vary enormously from processor to processor, and (especially) from memory architecture to memory architecture. In order to understand them all, we need first to understand the simplest among them, those that haunted the original 8088-based IBM PC. Later on in this book, I’ll be better able to explain the newer generation of cycle-eaters in terms of those ancestral cycle-eaters—but we have to get the groundwork down first.

-

The 8088’s Ancestral Cycle-Eaters

+

The 8088’s Ancestral Cycle-Eaters

Internally, the 8088 is a 16-bit processor, capable of running at full speed at all times—unless external data is required. External data must traverse the 8088’s external data bus and the PC’s data bus one byte at a time to and from peripherals, with cycle-eaters lurking along every step of the way. What’s more, external data includes not only memory operands but also instruction bytes, so even instructions with no memory operands can suffer from cycle-eaters. Since some of the 8088’s fastest instructions are register-only instructions, that’s important indeed.

@@ -85,16 +88,20 @@

The 8088 is internally a full 16-bit processor, equivalent to an 8086. (In fact, the 8086 is identical to the 8088, except that it has a full 16-bit bus. The 8088 is basically the poor man’s 8086, because it allows a cheaper—albeit slower—system to be built, thanks to the half-sized bus.) In terms of the instruction set, the 8088 is clearly a 16-bit processor, capable of performing any given 16-bit operation—addition, subtraction, even multiplication or division—with a single instruction. Externally, however, the 8088 is unequivocally an 8-bit processor, since the external data bus is only 8 bits wide. In other words, the programming interface is 16 bits wide, but the hardware interface is only 8 bits wide, as shown in Figure 4.2. The result of this mismatch is simple: Word-sized data can be transferred between the 8088 and memory or peripherals at only one-half the maximum rate of the 8086, which is to say one-half the maximum rate for which the Execution Unit of the 8088 was designed.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-02.html b/04-02.html index 3bc1f86..ad6024c 100644 --- a/04-02.html +++ b/04-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-


Figure 4.1
  The location of the major cycle-eaters in the IBM PC.

@@ -42,7 +45,7 @@

A related cycle-eater lurks beneath the 386SX chip, which is a 32-bit processor internally with only a 16-bit path to system memory. The numbers are different, but the way the cycle-eater operates is exactly the same. AT-compatible systems have 16-bit data buses, which can access a full 16-bit word at a time. The 386SX can process 32 bits (a doubleword) at a time, however, and loses a lot of time fetching that doubleword from memory in two halves.

-

The Impact of the 8-Bit Bus Cycle-Eater

+

The Impact of the 8-Bit Bus Cycle-Eater

One obvious effect of the 8-bit bus cycle-eater is that word-sized accesses to memory operands on the 8088 take 4 cycles longer than byte-sized accesses. That’s why the official instruction timings indicate that for code running on an 8088 an additional 4 cycles are required for every word-sized access to a memory operand. For instance,

@@ -70,7 +73,7 @@ add  byte ptr [MemVar],al
 
   

The upshot of all this is simply that the 8088 can transfer word-sized data to and from memory at only half the speed of the 8086, which inevitably causes performance problems when coupled with an Execution Unit that can process word-sized data every bit as quickly as an 8086. These problems show up with any code that uses word-sized memory operands. More ominously, as we will see shortly, the 8-bit bus cycle-eater can cause performance problems with other sorts of code as well.

-

What to Do about the 8-Bit Bus Cycle-Eater?

+

What to Do about the 8-Bit Bus Cycle-Eater?

The obvious implication of the 8-bit bus cycle-eater is that byte-sized memory variables should be used whenever possible. After all, the 8088 performs byte-sized memory accesses just as quickly as the 8086. For instance, Listing 4.1, which uses a byte-sized memory variable as a loop counter, runs in 10.03 s per loop. That’s 20 percent faster than the 12.05 µs per loop execution time of Listing 4.2, which uses a word-sized counter. Why the difference in execution times? Simply because each word-sized DEC performs 4 byte-sized memory accesses (two to read the word-sized operand and two to write the result back to memory), while each byte-sized DEC performs only 2 byte-sized memory accesses in all.

@@ -110,16 +113,20 @@ LoopTop:

I’d like to make a brief aside concerning code optimization in the listings in this book. Throughout this book I’ve modeled the sample code after working code so that the timing results are applicable to real-world programming. In Listings 4.1 and 4.2, for example, I could have shown a still greater advantage for byte-sized operands simply by performing 1,000 DEC instructions in a row, with no branching at all. However, DEC instructions don’t exist in a vacuum, so in the listings I used code that both decremented the counter and tested the result. The difference is that between decrementing a memory location (simply an instruction) and using a loop counter (a functional instruction sequence). If you come across code in this book that seems less than optimal, it’s simply due to my desire to provide code that’s relevant to real programming problems. On the other hand, optimal code is an elusive thing indeed; by no means should you assume that the code in this book is ideal! Examine it, question it, and improve upon it, for an inquisitive, skeptical mind is an important part of the Zen of assembly optimization.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-03.html b/04-03.html index c79e9b6..067e6d6 100644 --- a/04-03.html +++ b/04-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Back to the 8-bit bus cycle-eater. As I’ve said, in 8088 work you should strive to use byte-sized memory variables whenever possible. That does not mean that you should use 2 byte-sized memory accesses to manipulate a word-sized memory variable in preference to 1 word-sized memory access, as, for instance,

 mov  dl,byte ptr [MemVar]
@@ -105,16 +108,20 @@ shr  ax,1
 
   

should execute in 10 cycles, since each SHR takes 2 cycles to execute, according to Intel’s specifications. Those specifications contain Intel’s official instruction execution times, but in this case—and in many others—the specifications are drastically wrong. Why? Because they describe execution time once an instruction reaches the prefetch queue. They say nothing about whether a given instruction will be in the prefetch queue when it’s time for that instruction to run, or how long it will take that instruction to reach the prefetch queue if it’s not there already. Thanks to the low performance of the 8088’s external data bus, that’s a glaring omission—but, alas, an unavoidable one. Let’s look at why the official execution times are wrong, and why that can’t be helped.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-04.html b/04-04.html index 2435cf4..085c237 100644 --- a/04-04.html +++ b/04-04.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Official Execution Times Are Only Part of the Story

+

Official Execution Times Are Only Part of the Story

The sequence of 5 SHR instructions in the last example is 10 bytes long. That means that it can never execute in less than 24 cycles even if the 4-byte prefetch queue is full when it starts, since 6 instruction bytes would still remain to be fetched, at 4 cycles per fetch. If the prefetch queue is empty at the start, the sequence could take 40 cycles. In short, thanks to instruction fetching, the code won’t run at its documented speed, and could take up to four times longer than it is supposed to.

@@ -48,7 +51,7 @@

So now you know why the official instruction execution times are often wrong, and why Intel can’t provide better specifications. You also know now why it is that you must time your code if you want to know how fast it really is.

-

There Is No Such Beast as a True Instruction Execution Time

+

There Is No Such Beast as a True Instruction Execution Time

The effect of the code preceding an instruction on the execution time of that instruction makes the Zen timer trickier to use than you might expect, and complicates the interpretation of the results reported by the Zen timer. For one thing, the Zen timer is best used to time code sequences that are more than a few instructions long; below 10µs or so, prefetch queue effects and the limited resolution of the clock driving the timer can cause problems.

@@ -96,16 +99,20 @@

The key point is this: We’ve seen one code sequence in which SHR took 8-plus cycles to execute, and another in which it took only 2 cycles. Are we talking about two different forms of SHR here? Of course not—the difference is purely a reflection of the differing states in which the preceding code left the prefetch queue. In Listing 4.5, each SHR after the first few follows a slew of other SHR instructions which have sucked the prefetch queue dry, so overall performance reflects instruction fetch time. By contrast, each SHR in Listing 4.6 follows a MUL instruction which leaves the prefetch queue full, so overall performance reflects Execution Unit execution time.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-05.html b/04-05.html index 9c2b7a4..01b5506 100644 --- a/04-05.html +++ b/04-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Clearly, either instruction fetch time or Execution Unit execution time—or even a mix of the two, if an instruction is partially prefetched—can determine code performance. Some people operate under a rule of thumb by which they assume that the execution time of each instruction is 4 cycles times the number of bytes in the instruction. While that’s often true for register-only code, it frequently doesn’t hold for code that accesses memory. For one thing, the rule should be 4 cycles times the number of memory accesses, not instruction bytes, since all accesses take 4 cycles on the 8088-based PC. For another, memory-accessing instructions often have slower Execution Unit execution times than the 4 cycles per memory access rule would dictate, because the 8088 isn’t very fast at calculating memory addresses. Also, the 4 cycles per instruction byte rule isn’t true for register-only instructions that are already in the prefetch queue when the preceding instruction ends.

The truth is that it never hurts performance to reduce either the cycle count or the byte count of a given bit of code, but there’s no guarantee that one or the other will improve performance either. For example, consider Listing 4.7, which consists of a series of 4-cycle, 2-byte MOV AL,0 instructions, and which executes at the rate of 1.81 µs per instruction. Now consider Listing 4.8, which replaces the 4-cycle MOV AL,0 with the 3-cycle (but still 2-byte) SUB AL,AL, Despite its 1-cycle-per-instruction advantage, Listing 4.8 runs at exactly the same speed as Listing 4.7. The reason: Both instructions are 2 bytes long, and in both cases it is the 8-cycle instruction fetch time, not the 3 or 4-cycle Execution Unit execution time, that limits performance.

@@ -79,7 +82,7 @@

What we really want is to know how long useful working code takes to run, not how long a single instruction takes, and the Zen timer gives us the tool we need to gather that information. Granted, it would be easier if we could just add up neatly documented instruction execution times—but that’s not going to happen. Without actually measuring the performance of a given code sequence, you simply don’t know how fast it is. For crying out loud, even the people who designed the 8088 at Intel couldn’t tell you exactly how quickly a given 8088 code sequence executes on the PC just by looking at it! Get used to the idea that execution times are only meaningful in context, learn the rules of thumb in this book, and use the Zen timer to measure your code.

-

Approximating Overall Execution Times

+

Approximating Overall Execution Times

Don’t think that because overall instruction execution time is determined by both instruction fetch time and Execution Unit execution time, the two times should be added together when estimating performance. For example, practically speaking, each SHR in Listing 4.5 does not take 8 cycles of instruction fetch time plus 2 cycles of Execution Unit execution time to execute. Figure 4.3 shows that while a given SHR is executing, the fetch of the next SHR is starting, and since the two operations are overlapped for 2 cycles, there’s no sense in charging the time to both instructions. You could think of the extra instruction fetch time for SHR in Listing 4.5 as being 6 cycles, which yields an overall execution time of 8 cycles when added to the 2 cycles of Execution Unit execution time.

@@ -87,22 +90,26 @@

As a working definition, we’ll consider the execution time of a given instruction in a particular context to start when the first byte of the instruction is sent to the Execution Unit and end when the first byte of the next instruction is sent to the EU.

-

What to Do about the Prefetch Queue Cycle-Eater?

+

What to Do about the Prefetch Queue Cycle-Eater?

Reducing the impact of the prefetch queue cycle-eater is one of the overriding principles of high-performance assembly code. How can you do this? One effective technique is to minimize access to memory operands, since such accesses compete with instruction fetching for precious memory accesses. You can also greatly reduce instruction fetch time simply by your choice of instructions: Keep your instructions short. Less time is required to fetch instructions that are 1 or 2 bytes long than instructions that are 5 or 6 bytes long. Reduced instruction fetching lowers minimum execution time (minimum execution time is 4 cycles times the number of instruction bytes) and often leads to faster overall execution.

While short instructions minimize overall prefetch time, ironically they actually often suffer more from the prefetch queue bottleneck than do long instructions. Short instructions generally have such fast execution times that they drain the prefetch queue despite their small size. For example, consider the SHR of Listing 4.5, which runs at only 25 percent of its Execution Unit execution time even though it’s only 2 bytes long, thanks to the prefetch queue bottleneck. Short instructions are nonetheless generally faster than long instructions, thanks to the combination of fewer instruction bytes and faster Execution Unit execution times, and should be used as much as possible—just don’t expect them to run at their “official” documented speeds.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-06.html b/04-06.html index c3cacf3..8410871 100644 --- a/04-06.html +++ b/04-06.html @@ -1,5 +1,4 @@ - + @@ -19,22 +18,26 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

More than anything, the above rules mean using the registers as heavily as possible, both because register-only instructions are short and because they don’t perform memory accesses to read or write operands. However, using the registers is a rule of thumb, not a commandment. In some circumstances, it may actually be faster to access memory. (The look-up table technique is one such case.) What’s more, the performance of the prefetch queue (and hence the performance of each instruction) differs from one code sequence to the next, and can even differ during different executions of the same code sequence.

All in all, writing good assembler code is as much an art as a science. As a result, you should follow the rules of thumb described here—and then time your code to see how fast it really is. You should experiment freely, but always remember that actual, measured performance is the bottom line.

-

Holding Up the 8088

+

Holding Up the 8088

In this chapter I’ve taken you further and further into the depths of the PC, telling you again and again that you must understand the computer at the lowest possible level in order to write good code. At this point, you may well wonder, “Have we gotten low enough?”

@@ -54,7 +57,7 @@

All of the PC’s system memory consists of DRAM chips. Each DRAM chip in the PC must be completely refreshed about once every four milliseconds in order to ensure the integrity of the data it stores. Obviously, it’s highly desirable that the memory in the PC retain the correct data indefinitely, so each DRAM chip in the PC must always be refreshed within 4 µs of the last refresh. Since there’s no guarantee that a given program will access each and every DRAM block once every 4 µs, the PC contains special circuitry and programming for providing DRAM refresh.

-

How DRAM Refresh Works in the PC

+

How DRAM Refresh Works in the PC

On the original 8088-based IBM PC, timer 1 of the 8253 timer chip is programmed at power-up to generate a signal once every 72 cycles, or once every 15.08µs. That signal goes to channel 0 of the 8237 DMA controller, which requests the bus from the 8088 upon receiving the signal. (DMA stands for direct memory access, the ability of a device other than the 8088 to control the bus and access memory directly, without any help from the 8088.) As soon as the 8088 is between memory accesses, it gives control of the bus to the 8237, which in conjunction with special circuitry on the PC’s motherboard then performs a single 4-cycle read access to 1 of 256 possible addresses, advancing to the next address on each successive access. (The read access is only for the purpose of refreshing the DRAM; the data that is read isn’t used.)

@@ -65,16 +68,20 @@


Figure 4.5
  The PC bus dynamic RAM (DRAM) refresh.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-07.html b/04-07.html index c310a15..c5b759f 100644 --- a/04-07.html +++ b/04-07.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

The Impact of DRAM Refresh

+

The Impact of DRAM Refresh

Let’s look at examples from opposite ends of the spectrum in terms of the impact of DRAM refresh on code performance. First, consider the series of MUL instructions in Listing 4.9. Since a 16-bit MUL on the 8088 executes in between 118 and 133 cycles and is only 2 bytes long, there should be plenty of time for the prefetch queue to fill after each instruction, even after DRAM refresh has taken its slice of memory access time. Consequently, the prefetch queue should be able to keep the Execution Unit well-supplied with instruction bytes at all times. Since Listing 4.9 uses no memory operands, the Execution Unit should never have to wait for data from memory, and DRAM refresh should have no impact on performance. (Remember that the Execution Unit can operate normally during DRAM refreshes so long as it doesn’t need to request a memory access from the Bus Interface Unit.)

@@ -72,7 +75,7 @@

Which of the two cases we’ve examined reflects reality? While either case can happen, the latter case—significant performance reduction, ranging as high as 8.33 percent—is far more likely to occur. This is especially true for high-performance assembly code, which uses fast instructions that tend to cause non-stop instruction fetching.

-

What to Do About the DRAM Refresh Cycle-Eater?

+

What to Do About the DRAM Refresh Cycle-Eater?

Hmmm. When we discovered the prefetch queue cycle-eater, we learned to use short instructions. When we discovered the 8-bit bus cycle-eater, we learned to use byte-sized memory operands whenever possible, and to keep word-sized variables in registers. What can we do to work around the DRAM refresh cycle-eater?

@@ -88,16 +91,20 @@

Wait states are cycles during which a bus access by the CPU to a device on the PC’s bus is temporarily halted by that device while the device gets ready to complete the read or write. Wait states are well and truly the lowest level of code performance. Everything we have discussed (and will discuss)—even DMA accesses—can be affected by wait states.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-08.html b/04-08.html index 0a1d345..a3ee406 100644 --- a/04-08.html +++ b/04-08.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Wait states exist because the CPU must to be able to coexist with any adapter, no matter how slow (within reason). The 8088 expects to be able to complete each bus access—a memory or I/O read or write—in 4 cycles, but adapters can’t always respond that quickly for a number of reasons. For example, display adapters must split access to display memory between the CPU and the circuitry that generates the video signal based on the contents of display memory, so they often can’t immediately fulfill a request by the CPU for a display memory read or write. To resolve this conflict, display adapters can tell the CPU to wait during bus accesses by inserting one or more wait states, as shown in Figure 4.6. The CPU simply sits and idles as long as wait states are inserted, then completes the access as soon as the display adapter indicates its readiness by no longer inserting wait states. The same would be true of any adapter that couldn’t keep up with the CPU.

Mind you, this is all transparent to executing code. An instruction that encounters wait states runs exactly as if there were no wait states, only slower. Wait states are nothing more or less than wasted time as far as the CPU and your program are concerned.

@@ -69,16 +72,20 @@

Enough said. All the memory in the PC is not display memory, however, and unless you’re thickheaded enough to put code in display memory, the PC isn’t going to run as slowly as a PCjr. (Putting code or other non-video data in unused areas of display memory sounds like a neat idea—until you consider the effect on instruction prefetching of cutting the 8088’s already-poor memory access performance in half. Running your code from display memory is sort of like running on a hypothetical 8084—an 8086 with a 4-bit bus. Not recommended!) Given that your code and data reside in normal system memory below the 640K mark, how great an impact does the display adapter cycle-eater have on performance?

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-09.html b/04-09.html index 2ea20bc..bffae2b 100644 --- a/04-09.html +++ b/04-09.html @@ -1,5 +1,4 @@ - + @@ -19,20 +18,24 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The answer varies considerably depending on what display adapter and what display mode we’re talking about. The display adapter cycle-eater is worst with the Enhanced Graphics Adapter (EGA) and the original Video Graphics Array (VGA). (Many VGAs, especially newer ones, insert many fewer wait states than IBM’s original VGA. On the other hand, Super VGAs have more bytes of display memory to be accessed in high-resolution mode.) While the Color/Graphics Adapter (CGA), Monochrome Display Adapter (MDA), and Hercules Graphics Card (HGC) all suffer from the display adapter cycle-eater as well, they suffer to a lesser degree. Since the VGA represents the base standard for PC graphics now and for the foreseeable future, and since it is the hardest graphics adapter to wring performance from, we’ll restrict our discussion to the VGA (and its close relative, the EGA) for the remainder of this chapter.

-

The Impact of the Display Adapter Cycle-Eater

+

The Impact of the Display Adapter Cycle-Eater

Even on the EGA and VGA, the effect of the display adapter cycle-eater depends on the display mode selected. In text mode, the display adapter cycle-eater is rarely a major factor. It’s not that the cycle-eater isn’t present; however, a mere 4,000 bytes control the entire text mode display, and even with the display adapter cycle-eater it just doesn’t take that long to manipulate 4,000 bytes. Even if the display adapter cycle-eater were to cause the 8088 to take as much as 5µs per display memory access—more than five times normal—it would still take only 4,000x 2x 5µs, or 40 µs, to read and write every byte of display memory. That’s a lot of time as measured in 8088 cycles, but it’s less than the blink of an eye in human time, and video performance only matters in human time. After all, the whole point of drawing graphics is to convey visual information, and if that information can be presented faster than the eye can see, that is by definition fast enough.

@@ -102,16 +105,20 @@

Bear in mind that we’re talking about a worst case here; the impact of the display adapter cycle-eater is proportional to the percent of time a given code sequence spends accessing display memory.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/04-10.html b/04-10.html index 9106ff7..1e8ec5c 100644 --- a/04-10.html +++ b/04-10.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- @@ -42,7 +45,7 @@

Nonetheless, the display adapter cycle-eater always takes its toll on graphics code. Interestingly, that toll becomes much higher on ATs and 80386 machines because while those computers can execute many more instructions per microsecond than can the 8088-based PC, it takes just as long to access display memory on those computers as on the 8088-based PC. Remember, the limited speed of access to a graphics adapter is an inherent characteristic of the adapter, so the fastest computer around can’t access display memory one iota faster than the adapter will allow.

-

What to Do about the Display Adapter Cycle-Eater?

+

What to Do about the Display Adapter Cycle-Eater?

What can we do about the display adapter cycle-eater? Well, we can minimize display memory accesses whenever possible. In particular, we can try to avoid read/modify/write display memory operations of the sort used to mask individual pixels and clip images. Why? Because read/modify/write operations require two display memory accesses (one read and one write) each time display memory is manipulated. Instead, we should try to use writes of the sort that set all the pixels in a given byte of display memory at once, since such writes don’t require accompanying read accesses. The key here is that only half as many display memory accesses are required to write a byte to display memory as are required to read a byte from display memory, mask part of it off and alter the rest, and write the byte back to display memory. Half as many display memory accesses means half as many display memory wait states.

@@ -60,7 +63,7 @@

It would be handy to explore the display adapter cycle-eater issue in depth, with lots of example code and execution timings, but alas, I don’t have the space for that right now. For the time being, all you really need to know about the display adapter cycle-eater is that on the 8088 you can lose more than 8 cycles of execution time on each access to display memory. For intensive access to display memory, the loss really can be as high as 8cycles (and up to 50, 100, or even more on 486s and Pentiums paired with slow VGAs), while for average graphics code the loss is closer to 4 cycles; in either case, the impact on performance is significant. There is only one way to discover just how significant the impact of the display adapter cycle-eater is for any particular graphics code, and that is of course to measure the performance of that code.

-

Cycle-Eaters: A Summary

+

Cycle-Eaters: A Summary

We’ve covered a great deal of sophisticated material in this chapter, so don’t feel bad if you haven’t understood everything you’ve read; it will all become clear from further reading, especially once you study, time, and tune code that you have written yourself. What’s really important is that you come away from this chapter understanding that on the 8088:

@@ -76,20 +79,24 @@

This basic knowledge about cycle-eaters puts you in a good position to understand the results reported by the Zen timer, and that means that you’re well on your way to writing high-performance assembler code.

-

What Does It All Mean?

+

What Does It All Mean?

There you have it: life under the programming interface. It’s not a particularly pretty picture for the inhabitants of that strange realm where hardware and software meet are little-known cycle-eaters that sap the speed from your unsuspecting code. Still, some of those cycle-eaters can be minimized by keeping instructions short, using the registers, using byte-sized memory operands, and accessing display memory as little as possible. None of the cycle-eaters can be eliminated, and dynamic RAM refresh can scarcely be addressed at all; still, aren’t you better off knowing how fast your code really runs—and why—than you were reading the official execution times and guessing? And while specific cycle-eaters vary in importance on later x86-family processors, with some cycle-eaters vanishing altogether and new ones appearing, the concept that understanding these obscure gremlins is a key to performance remains unchanged, as we’ll see again and again in later chapters.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/05-01.html b/05-01.html index 343592e..a4b0944 100644 --- a/05-01.html +++ b/05-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 5
Crossing the Border

@@ -59,7 +62,7 @@

And with that, let’s look at a fairly complex application of restartable blocks.

-

Searching for Text

+

Searching for Text

The application we’re going to examine searches a file for a specified string. We’ll develop a program that will search the file specified on the command line for a string (also specified on the comline), then report whether the string was found or not. (Because the searched-for string is obtained via argv, it can’t contain any whitespace characters.)

@@ -75,16 +78,20 @@

The easiest approach would be to use a C/C++ library function. The closest match to what we need is strstr(), which searches one string for the first occurrence of a second string. However, while strstr() would work, it isn’t ideal for our purposes. The problem is this: Where we want to search a fixed-length buffer for the first occurrence of a string, strstr() searches a string for the first occurrence of another string.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/05-02.html b/05-02.html index a8ca012..96e7001 100644 --- a/05-02.html +++ b/05-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

We could put a zero byte at the end of our buffer to allow strstr() to work, but why bother? The strstr() function must spend time either checking for the end of the string being searched or determining the length of that string—wasted effort given that we already know exactly how long our search buffer is. Even if a given strstr() implementation is well-written, its performance will suffer, at least for our application, from unnecessary overhead.

@@ -64,7 +67,7 @@

Now that we’ve selected a searching approach, let’s integrate it with file handling and searching through multiple blocks. In other words, let’s make it restartable.

-

Making a Search Restartable

+

Making a Search Restartable

As it happens, there’s no great trick to putting the pieces of this search program together. Basically, we’ll read in a buffer of data (we’ll work with 16K at a time to avoid signed overflow problems with integers), search it for a match with the memchr()/memcmp() engine described, and exit with a “string found” response if the desired string is found.

@@ -76,16 +79,20 @@

Listing 5.1 nicely illustrates the core concept of restartable blocks: Organize your program so that you can do your processing within each block as fast as you could if there were only one block—which is to say at top speed—and make your blocks as large as possible in order to minimize the overhead associated with going from one block to the next.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/05-03.html b/05-03.html index 6e76f01..7c49a0b 100644 --- a/05-03.html +++ b/05-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 5.1 SEARCH.C

 /* Program to search the file specified by the first command-line
@@ -199,16 +202,20 @@ main(int argc, char *argv[]) {
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/05-04.html b/05-04.html index ec1e233..d580675 100644 --- a/05-04.html +++ b/05-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Interpreting Where the Cycles Go

To boost the overall performance of Listing 5.1, I would normally convert SearchForString() to assembly language at this point. However, I’m not going to do that, and the reason is as important a lesson as any discussion of optimized assembly code is likely to be. Take a moment to examine some interesting performance aspects of the C implementation, and all should become much clearer.

@@ -46,7 +49,7 @@

Not likely.

-

Knowing When Assembly Is Pointless

+

Knowing When Assembly Is Pointless

So that’s why we’re not going to go to assembly language in this example—which is not to say it would never be worth converting the search engine in Listing 5.1 to assembly.

@@ -54,16 +57,20 @@

In contrast, Listing 5.1 must return from memchr(), set up parameters, and call memcmp() in order to do the same thing. Likewise, assembly can switch back to REPNZ SCASB after a non-match much more quickly than Listing 5.1. The switching overhead is high; when searching a file completely filled with the character z for the string “zy,” Listing 5.1 takes almost 1/2 minute, or nearly an order of magnitude longer than when searching a file filled with normal text.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/05-05.html b/05-05.html index c00fcea..f4ddf4a 100644 --- a/05-05.html +++ b/05-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

It might also be worth converting the search engine to assembly for searches performed entirely in memory; with the overhead of file access eliminated, improvements in search-engine performance would translate directly into significantly faster overall performance. One such application that would have much the same structure as Listing 5.1 would be searching through expanded memory buffers, and another would be searching through huge (segment-spanning) buffers.

And so we find, as we so often will, that optimization is definitely not a cut-and-dried matter, and that there is no such thing as a single “best” approach.

@@ -66,16 +69,20 @@

Would you?

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/06-01.html b/06-01.html index 7e41cbb..e40329b 100644 --- a/06-01.html +++ b/06-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 6
Looking Past Face Value

@@ -59,7 +62,7 @@

In short, the x86 family can do much more than you think—if you’ll use everything it has to offer. Give it a shot!

-

Memory Addressing and Arithmetic

+

Memory Addressing and Arithmetic

Years ago, I saw a clip on the David Letterman show in which Letterman walked into a store by the name of “Just Lamps” and asked, “So what do you sell here?”

@@ -86,16 +89,20 @@ mov al,[bx] mov al,[bx+si]
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/06-02.html b/06-02.html index ae946b7..29fce31 100644 --- a/06-02.html +++ b/06-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The two approaches are functionally interchangeable but not equivalent from a performance standpoint, and which is better depends on the particular context. If it’s a one-shot memory access, it’s best to let the processor perform the addition; it’s generally faster at doing this than a separate ADD instruction would be. If it’s a memory access within a loop, however, it’s advantageous on the 8088 CPU to perform the addition outside the loop, if possible, reducing effective address calculation time inside the loop, as in the following:

       add   bx,si
@@ -83,7 +86,7 @@ lea  di,[si+2]
   


Figure 6.1
  Operation of ADD Reg,Reg vs. LEA Reg,{Addr}.

-

The Wonders of LEA on the 386

+

The Wonders of LEA on the 386

LEA really comes into its own as a “super-ADD” instruction on the 386, 486, and Pentium, where it can take advantage of the enhanced memory addressing modes of those processors. (The 486 and Pentium offer the same modes as the 386, so I’ll refer only to the 386 from now on.) The 386 can do two very interesting things: It can use any 32-bit register (EAX, EBX, and so on) as the memory addressing base register and/or the memory addressing index register, and it can multiply any 32-bit register used as an index by two, four, or eight in the process of calculating a memory address, as shown in Figure 6.2. Let’s see what that’s good for.

@@ -137,16 +140,20 @@ add ebx,edx

I’d like to extend my thanks to Duane Strong of Metagraphics for his help in brainstorming uses for the 386 version of LEA and for pointing out the complications of 486 instruction timings.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/07-01.html b/07-01.html index 6a41bd7..cea1868 100644 --- a/07-01.html +++ b/07-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 7
Local Optimization

@@ -67,7 +70,7 @@

And yes, in case you’re wondering, the above story is indeed true. Was I there? Let me put it this way: If I were, I’d never admit it!

-

When LOOP Is a Bad Idea

+

When LOOP Is a Bad Idea

Let’s examine first an instruction that is less than it appears to be: LOOP. There’s no mystery about what LOOP does; it decrements CX and branches if CX doesn’t decrement to zero. It’s so beautifully suited to the task of counting down loops that any experienced x86 programmer instinctively stuffs the loop count in CX and reaches for LOOP when setting up a loop. That’s fine—LOOP does, of course, work as advertised—but there is one problem:

@@ -89,16 +92,20 @@ -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/07-02.html b/07-02.html index c0a9229..86eb43f 100644 --- a/07-02.html +++ b/07-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

By the way, don’t fall victim to the lures of JCXZ and do something like this:

 and     cx,ofh          ;Isolate the desired field
@@ -50,7 +53,7 @@ jz      SkipLoop         ;If field is 0, don’t bother
 
   

In particular, if you’re going to write 386 protected mode code, which will run only on the 386, 486, and Pentium, you’d be well advised to rethink your use of the more esoteric members of the x86 instruction set. LOOP, JCXZ, the various accumulator-specific instructions, and even the string instructions in many circumstances no longer offer the advantages they did on the 8088. Sometimes they’re just not any faster than more general instructions, so they’re not worth going out of your way to use; sometimes, as with LOOP, they’re actually slower, and you’d do well to avoid them altogether in the 386/486 world. Reviewing the instruction cycle times in the MASM or TASM manuals, or looking over the cycle times in Intel’s literature, is a good place to start; published cycle times are closer to actual execution times on the 386 and 486 than on the 8088, and are reasonably reliable indicators of the relative performance levels of x86 instructions.

-

Avoiding LOOPS of Any Stripe

+

Avoiding LOOPS of Any Stripe

Cycle counting and directly substituting instructions (DEC CX/JNZ for LOOP, for example) are techniques that belong at the lowest level of optimization. It’s an important level, but it’s fairly mechanical; once you’ve learned the capabilities and relative performance levels of the various instructions, you should be able to select the best instructions fairly easily. What’s more, this is a task at which compilers excel. What I’m saying is that you shouldn’t get too caught up in counting cycles because that’s a small (albeit important) part of the optimization picture, and not the area in which your greatest advantage lies.

@@ -74,16 +77,20 @@ jz SkipLoop ;If field is 0, don’t bother

When the LOOP in Listing 7.1 is replaced with DEC CX/JNZ, performance improves to 168 µs, less than 2 percent faster than Listing 7.1. Actually, instruction fetching, instruction alignment, cache characteristics, or something similar is affecting these results; I’d expect a slightly larger improvement—around 7 percent—but that’s the most that counting cycles could buy us in this case. (All right, already; LOOPNZ could be used at the bottom of the loop, and other optimizations are surely possible, but all that won’t add up to anywhere near the benefits we’re about to see from local optimization, and that’s the whole point.)

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/07-03.html b/07-03.html index 455846e..e57cfd1 100644 --- a/07-03.html +++ b/07-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 7.1 L7-1.ASM

 ; Program to illustrate searching through a buffer of a specified
@@ -128,16 +131,20 @@ SearchMaxLengthendp
 
   

Listing 7.2 takes a different tack, unrolling the loop so that four bytes are checked for each LOOP performed. The same instructions are used inside the loop in each listing, but Listing 7.2 is arranged so that three-quarters of the LOOPs are eliminated. Listings 7.1 and 7.2 perform exactly the same task, and they use the same instructions in the loop—the searching algorithm hasn’t changed in any way—but we have sequenced the instructions differently in Listing 7.2, and that makes all the difference.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/07-04.html b/07-04.html index 0730f35..162d8f4 100644 --- a/07-04.html +++ b/07-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 7.2 L7-2.ASM

 ; Program to illustrate searching through a buffer of a specified
@@ -172,16 +175,20 @@ SearchMaxLengthendp
     
   
 
-  


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/07-05.html b/07-05.html index 8cf1670..fca3f6f 100644 --- a/07-05.html +++ b/07-05.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Rotating and Shifting with Tables

+

Rotating and Shifting with Tables

As another example of local optimization, consider the matter of rotating or shifting a mask into position. First, let’s look at the simple task of setting bit N of AX to 1.

@@ -80,7 +83,7 @@ BIT_PATTERN=BIT_PATTERN SHL 1 -

NOT Flips Bits—Not Flags

+

NOT Flips Bits—Not Flags

The NOT instruction flips all the bits in the operand, from 0 to 1 or from 1 to 0. That’s as simple as could be, but NOT nonetheless has a minor but interesting talent: It doesn’t affect the flags. That can be irritating; I once spent a good hour tracking down a bug caused by my unconscious assumption that NOT does set the flags. After all, every other arithmetic and logical instruction sets the flags; why not NOT? Probably because NOT isn’t considered to be an arithmetic or logical instruction at all; rather, it’s a data manipulation instruction, like MOV and the various rotates. (These are RCR, RCL, ROR, and ROL, which affect only the Carry and Overflow flags.) NOT is often used for tasks, such as flipping masks, where there’s no reason to test the state of the result, and in that context it can be handy to keep the flags unmodified for later testing.

@@ -94,7 +97,7 @@ BIT_PATTERN=BIT_PATTERN SHL 1

The x86 instruction set offers many ways to accomplish almost any task. Understanding the subtle distinctions between the instructions—whether and which flags are set, for example—can be critical when you’re trying to optimize a code sequence and you’re running out of registers, or when you’re trying to minimize branching.

-

Incrementing with and without Carry

+

Incrementing with and without Carry

Another case in which there are two slightly different ways to perform a task involves adding 1 to an operand. You can do this with INC, as in INC AX, or you can do it with ADD, as in ADD AX,1. What’s the difference? The obvious difference is that INC is usually a byte or two shorter (the exception being ADD AL,1, which at two bytes is the same length as INC AL), and is faster on some processors. Less obvious, but no less important, is that ADD sets the Carry flag while INC leaves the Carry flag untouched.

@@ -158,16 +161,20 @@ ADC DX,0

As always, pay attention!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/08-01.html b/08-01.html index 1e21b85..ce4223f 100644 --- a/08-01.html +++ b/08-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 8
Speeding Up C with Assembly Language

@@ -55,7 +58,7 @@

Apropos of which, when was the last time you heard of Terry Jacks?

-

Billy, Don’t Be a Compiler

+

Billy, Don’t Be a Compiler

The key to optimizing C programs with assembly language is, as always, writing good assembly language code, but with an added twist. Rule 1 when converting C code to assembly is this: Don’t think like a compiler. That’s more easily said than done, especially when the C code you’re converting is readily available as a model and the assembly code that the compiler generates is available as well. Nevertheless, the principle of not thinking like a compiler is essential, and is, in one form or another, the basis for all that I’ll discuss below.

@@ -81,16 +84,20 @@

What this means is that when you want to speed up a portion of a C program, you should identify the entire critical portion and move all of that critical portion into an assembly language function. You don’t want to move a part of the inner loop into assembly language and then call it from C every time through the loop; the function call and return overhead would be unacceptable. Carve out the critical code en masse and move it into assembly, and try to avoid calls and returns even in your assembly code. True, in assembly you can pass parameters in registers, but the calls and returns themselves are still slow; if the extra cycles they take don’t affect performance, then the code they’re in probably isn’t critical, and perhaps you’ve chosen to convert too much code to assembly, eh?

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/08-02.html b/08-02.html index b8d55f3..16bcb40 100644 --- a/08-02.html +++ b/08-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Stack Frames Slow So Much

C compilers work within the stack frame model, whereby variables reside in a block of stack memory and are accessed via offsets from BP. Compilers may store a couple of variables in registers and may briefly keep other variables in registers when they’re used repeatedly, but the stack frame is the underlying architecture. It’s a nice architecture; it’s flexible, convenient, easy to program, and makes for fairly compact code. However, stack frames have a few drawbacks. They must be constructed and destroyed, which takes both time and code. They are so easy to use that they tend to bias the assembly language programmer in favor of accessing memory variables more often than might be necessary. Finally, you cannot use BP as a general-purpose register if you intend to access a stack frame, and having that seventh register available is sometimes useful indeed.

@@ -50,7 +53,7 @@

In assembly language you have full control over segments. Use it, and, if necessary, reorganize your code to minimize segment loading.

-

Why Speeding Up Is Hard to Do

+

Why Speeding Up Is Hard to Do

You might think that the most obvious advantage assembly language has over C is that it allows the use of all forms of instructions and all registers in all ways, whereas C compilers tend to use a subset of registers and instructions in a limited number of ways. Yes and no. It’s true that C compilers typically don’t generate instructions such as XLAT, rotates, or the string instructions. On the other hand, XLAT and rotates are useful in a limited set of circumstances, and string instructions are used in the C library functions. In fact, C library code is likely to be carefully optimized by experts, and may be much better than equivalent code you’d produce yourself.

@@ -113,20 +116,24 @@ jz Match

That said, let me show some of these precepts in action.

-

A C-to-Assembly Case Study

+

A C-to-Assembly Case Study

Listing 8.1 is the sample C application I’m going to use to examine optimization in action. Listing 8.1 isn’t really complete—it doesn’t handle the “no-matches” case well, and it assumes that the sum of all matches will fit into an int—but it will do just fine as an optimization example.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/08-03.html b/08-03.html index 4e12a5e..707661a 100644 --- a/08-03.html +++ b/08-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 8.1 L8-1.C

 /* Program to search an array spanning a linked list of variable-
@@ -193,16 +196,20 @@ $I265:
 $FB264:
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/08-04.html b/08-04.html index 0fd1acc..b6365c7 100644 --- a/08-04.html +++ b/08-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

It’s hard to squeeze much more performance from this code by tweaking it, as exemplified by Listing 8.3, a fine-tuned assembly version of FindIDAverage that was produced by looking at the assembly output of MS C/C++ and tightening it. Listing 8.3 eliminates all stack frame access in the inner loop, but that’s about all the tightening there is to do. The result, as shown in Table 8.1, is that Listing 8.3 runs a modest 11 percent faster than Listing 8.1 on a 386. The results could vary considerably, depending on the nature of the data set searched through (average block size and frequency of matches). But, then, understanding the typical and worst case conditions is part of optimization, isn’t it?

LISTING 8.3 L8-3.ASM

@@ -290,16 +293,20 @@ _FindIDAverage ENDP end
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/08-05.html b/08-05.html index 1f332c0..1cf862d 100644 --- a/08-05.html +++ b/08-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listings 8.5 and 8.6 together go the final step and change the rules in favor of assembly language. Listing 8.5 creates the same list of linked blocks as Listing 8.1. However, instead of storing an array of structures within each block, it stores two arrays in each block, one consisting of ID numbers and the other consisting of the corresponding values, as shown in Figure 8.3. No information is lost; the data is merely rearranged.

LISTING 8.5 L8-5.C

@@ -186,16 +189,20 @@ _FindIDAverage2 ENDP

I trust you get the picture. The sort of instruction-by-instruction optimization that so many of us love to do as a kind of puzzle is fun, but compilers can do it nearly as well as you can, and in the future will surely do it better. What a compiler can’t do is tie together the needs of the program specification on the high end and the processor on the low end, resulting in critical code that runs just about as fast as the hardware permits. The only software that can do that is located north of your sternum and slightly aft of your nose. Dust it off and put it to work—and your code will never again be confused with anything by Hamilton, Joe, Frank, eynolds or Bo Donaldson and the Heywoods.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/09-01.html b/09-01.html index 5cfafb6..e14ba24 100644 --- a/09-01.html +++ b/09-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 9
Hints My Readers Gave Me

@@ -64,7 +67,7 @@ X - 1 = 0

I like to think I know more about performance programming than Barry knew about math. Nonetheless, I always welcome good ideas and comments, and many readers have sent me a slew of those over the years. So in this chapter, I think I’ll return the favor by devoting a chapter to reader feedback.

-

Another Look at LEA

+

Another Look at LEA

Several people have pointed out that while LEA is great for performing certain additions (see Chapter 6), it isn’t a perfect replacement for ADD. What’s the difference? LEA, an addressing instruction by trade, doesn’t affect the flags, while the arithmetic ADD instruction most certainly does. This is no problem when performing additions that involve only quantities that fit in one machine word (32 bits in 386 protected mode, 16 bits otherwise), but it renders LEA useless for multiword operations, which use the Carry flag to tie together partial results. For example, these instructions

@@ -100,7 +103,7 @@ ADDLOOP:
 
   

But there sure are a lot of interesting options, aren’t there?

-

The Kennedy Portfolio

+

The Kennedy Portfolio

Reader John Kennedy regularly passes along intriguing assembly programming tricks, many of which I’ve never seen mentioned anywhere else. John likes to optimize for size, whereas I lean more toward speed, but many of his optimizations are good for both purposes. Here are a few of my favorites:

@@ -140,16 +143,20 @@ REP MOVSB ;copy any odd byte CopyDone:
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/09-02.html b/09-02.html index 7c7f550..8c644d2 100644 --- a/09-02.html +++ b/09-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

However, it generally is. Sure, if the length is odd, John’s approach incurs a penalty approximately equal to the REP startup time for MOVSB. However, if the length is even, John’s approach doesn’t branch, saving cycles and not emptying the prefetch queue. If copy lengths are evenly distributed between even and odd, John’s approach is faster in most x86 systems. (Not on the 486, though.)

John also points out that on the 386, multiple LEAs can be combined to perform multiplications that can’t be handled by a single LEA, much as multiple shifts and adds can be used for multiplication, only faster. LEA can be used to multiply in a single instruction on the 386, but only by the values 2, 3, 4, 5, 8, and 9; several LEAs strung together can handle a much wider range of values. For example, video programmers are undoubtedly familiar with the following code to multiply AX times 80 (the width in bytes of the bitmap in most PC display modes):

@@ -66,7 +69,7 @@ SHL AX,2 ;*64 ADD AX,BX ;*80
-

Speeding Up Multiplication

+

Speeding Up Multiplication

That brings us to multiplication, one of the slowest of x86 operations and one that allows for considerable optimization. One way to speed up multiplication is to use shift and add, LEA, or a lookup table to hard-code a multiplication operation for a fixed multiplier, as shown above. Another is to take advantage of the early-out feature of the 386 (and the 486, but in the interests of brevity I’ll just say “386” from now on) by arranging your operands so that the multiplier (always the rightmost operand following MUL or IMUL) is no larger than the other operand.

@@ -96,7 +99,7 @@ ADD AX,BX ;*80

That doesn’t mean that your code should test and swap operands to make sure the smaller one is the multiplier; that rarely pays off. I’m speaking more of the case where you’re scaling an array up by a value that’s always in the range of, say, 2 to 10; because the scale value will always be small and the array elements may have any value, the scale value is the logical choice for the multiplier.

-

Optimizing Optimized Searching

+

Optimizing Optimized Searching

Rob Williams writes with a wonderful optimization to the REPNZ SCASB-based optimized searching routine I discussed in Chapter 5. As a quick refresher, I described searching a buffer for a text string as follows: Scan for the first byte of the text string with REPNZ SCASB, then use REPZ CMPS to check for a full match whenever REPNZ SCASB finds a match for the first character, as shown in Figure 9.1. The principle is that most buffer characters won’t match the first character of any given string, so REPNZ SCASB, by far the fastest way to search on the PC, can be used to eliminate most potential matches; each remaining potential match can then be checked in its entirety with REPZ CMPS.

@@ -115,16 +118,20 @@ ADD AX,BX ;*80

Imagine, if you will, that you’re searching for the string “EQUAL.” By my approach, you’d use REPNZ SCASB to scan for each occurrence of “E,” which crops up quite often in normal text. Rob points out that it would make more sense to scan for “Q,” then back up one character and check the whole string when a “Q” is found, as shown in Figure 9.2. “Q” is likely to occur much less often, resulting in many fewer whole-string checks and much faster processing.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/09-03.html b/09-03.html index 6c7fb6d..cde7f77 100644 --- a/09-03.html +++ b/09-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 9.1 implements the scan-on-first-character approach. Listing 9.2 scans for whatever character the caller specifies. Listing 9.3 is a test program used to compare the two approaches. How much difference does Rob’s revelation make? Plenty. Even when the entire C function call to FindString is timed—strlen calls, parameter pushing, calling, setup, and all—the version of FindString in Listing 9.2, which is directed by Listing 9.3 to scan for the infrequently-occurring “Q,” is about 40 percent faster on a 20 MHz cached 386 for the test search of Listing 9.3 than is the version of FindString in Listing 9.1, which always scans for the first character, in this case “E.” However, when only the search loops (the code that actually does the searching) in the two versions of FindString are compared, Listing 9.2 is more than twice as fast as Listing 9.1—a remarkable improvement over code that already uses REPNZ SCASB and REPZ CMPS.

What I like so much about Rob’s approach is that it demonstrates that optimization involves much more than instruction selection and cycle counting. Listings 9.1 and 9.2 use pretty much the same instructions, and even use the same approach of scanning with REPNZ SCASB and using REPZ CMPS to check scanning matches.

@@ -140,16 +143,20 @@ _FindStringendp end
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/09-04.html b/09-04.html index 80ba8d9..54d4bcc 100644 --- a/09-04.html +++ b/09-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 9.2 L9-2.ASM

 ; Searches a text buffer for a text string. Uses REPNZ SCASB to scan
@@ -166,16 +169,20 @@ void main() {
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/09-05.html b/09-05.html index 7497267..a4ea2bf 100644 --- a/09-05.html +++ b/09-05.html @@ -1,5 +1,4 @@ - + @@ -19,22 +18,26 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

You’ll notice that in Listing 9.2 I didn’t use a table of character frequencies in English text to determine the character for which to scan, but rather let the caller make that choice. Each buffer of bytes has unique characteristics, and English-letter frequency could well be inappropriate. What if the buffer is filled with French text? Cyrillic? What if it isn’t text that’s being searched? It might be worthwhile for an application to build a dynamic frequency table for each buffer so that the best scan character could be chosen for each search. Or perhaps not, if the search isn’t time-critical or the buffer is small.

The point is that you can improve performance dramatically by understanding the nature of the data with which you work. (This is equally true for high-level language programming, by the way.) Listing 9.2 is very similar to and only slightly more complex than Listing 9.1; the difference lies not in elbow grease or cycle counting but in the organic integrating optimizer technology we all carry around in our heads.

-

Short Sorts

+

Short Sorts

David Stafford (recently of Borland and Borland Japan) who happens to be one of the best assembly language programmers I’ve ever met, has written a C-callable routine that sorts an array of integers in ascending order. That wouldn’t be particularly noteworthy, except that David’s routine, shown in Listing 9.4, is exactly 25 bytes long. Look at the code; you’ll keep saying to yourself, “But this doesn’t work...oh, yes, I guess it does.” As they say in the Prego spaghetti sauce ads, it’s in there—and what a job of packing. Anyway, David says that a 24-byte sort routine eludes him, and he’d like to know if anyone can come up with one.

@@ -74,7 +77,7 @@ _sort: pop dx ;get return address (entry point) end
-

Full 32-Bit Division

+

Full 32-Bit Division

One of the most annoying limitations of the x86 is that while the dividend operand to the DIV instruction can be 32 bits in size, both the divisor and the result must be 16 bits. That’s particularly annoying in regards to the result because sometimes you just don’t know whether the ratio of the dividend to the divisor is greater than 64K-1 or not—and if you guess wrong, you get that godawful Divide By Zero interrupt. So, what is one to do when the result might not fit in 16 bits, or when the dividend is larger than 32 bits? Fall back to a software division approach? That will work—but oh so slowly.

@@ -87,16 +90,20 @@ _sort: pop dx ;get return address (entry point)

As for handling signed division with arbitrarily large dividends, that can be done easily enough by remembering the signs of the dividend and divisor, dividing the absolute value of the dividend by the absolute value of the divisor, and applying the stored signs to set the proper signs for the quotient and remainder. There may be more clever ways to produce the same result, by using IDIV, for example; if you know of one, drop me a line c/o Coriolis Group Books.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/09-06.html b/09-06.html index 4eb5c48..bdfe103 100644 --- a/09-06.html +++ b/09-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 9.5 L9-5.ASM

 ; Divides an arbitrarily long unsigned dividend by a 16-bit unsigned
@@ -120,7 +123,7 @@ main() {
 }
 
-

Sweet Spot Revisited

+

Sweet Spot Revisited

Way back in Volume 1, Number 1 of PC TECHNIQUES, (April/May 1990) I wrote the very first of that magazine’s HAX (#1), which extolled the virtues of placing your most commonly-used automatic (stack-based) variables within the stack’s “sweet spot,” the area between +127 to -128 bytes away from BP, the stack frame pointer. The reason was that the 8088 can store addressing displacements that fall within that range in a single byte; larger displacements require a full word of storage, increasing code size by a byte per instruction, and thereby slowing down performance due to increased instruction fetching time.

@@ -138,16 +141,20 @@ main() {

In assembly, it’s easy to control the organization of your stack frame. In C, however, you’ll have to figure out the allocation scheme your compiler uses to allocate automatic variables, and declare automatics appropriately to produce the desired effect. It can be done: I did it in Turbo C some years back, and trimmed the size of a program (admittedly, a large one) by several K—not bad, when you consider that the “sweet spot” optimization is essentially free, with no code reorganization, change in logic, or heavy thinking involved.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/09-07.html b/09-07.html index b0a7171..280376d 100644 --- a/09-07.html +++ b/09-07.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Hard-Core Cycle Counting

+

Hard-Core Cycle Counting

Next, we come to an item that cycle counters will love, especially since it involves apparently incorrect documentation on Intel’s part. According to Intel’s documents, all RCR and RCL instructions, which perform rotations through the Carry flag, as shown in Figure 9.4, take 9 cycles on the 386 when working with a register operand. My measurements indicate that the 9-cycle execution time almost holds true for multibit rotate-through-carries, which I’ve timed at 8 cycles apiece; for example, RCR AX,CL takes 8 cycles on my 386, as does RCL DX,2. Contrast that with ROR and ROL, which can rotate the contents of a register any number of bits in just 3 cycles.

@@ -43,7 +46,7 @@

No great lesson here, just a caution to be leery of multibit RCR and RCL when performance matters—and to take cycle-time documentation with a grain of salt.

-

Hardwired Far Jumps

+

Hardwired Far Jumps

Did you ever wonder how to code a far jump to an absolute address in assembly language? Probably not, but if you ever do, you’re going to be glad for this next item, because the obvious solution doesn’t work. You might think all it would take to jump to, say, 1000:5 would be JMP FAR PTR 1000:5, but you’d be wrong. That won’t even assemble. You might then think to construct in memory a far pointer containing 1000:5, as in the following:

@@ -84,7 +87,7 @@ start:
 
   

If the obvious doesn’t work (and it usually doesn’t), just try everything you can think of, no matter how ridiculous, until you find something that does—a rule with plenty of history on its side.

-

Setting 32-Bit Registers: Time versus Space

+

Setting 32-Bit Registers: Time versus Space

To finish up this chapter, consider these two items. First, in 32-bit protected mode,

@@ -111,16 +114,20 @@ move  bx,-1
 
   

Be warned, though, that I’ve found OR, AND, ADD, and the like to be a cycle slower than MOV when working with immediate operands on the 386 under some circumstances, for reasons that thus far escape me. This just reinforces the first rule of optimization: Measure your code in action, and place not your trust in documented cycle times.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/10-01.html b/10-01.html index 91b8f25..eedc7a5 100644 --- a/10-01.html +++ b/10-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 10
Patient Coding, Faster Code

@@ -57,7 +60,7 @@

In this chapter, I’m going to walk you through a simple but illustrative case history that nicely points up the wisdom of delaying gratification when faced with programming problems, so that your mind has time to chew on the problems from other angles. The alternative solutions you find by doing this may seem obvious, once you’ve come up with them. They may not even differ greatly from your initial solutions. Often, however, they will be much better—and you’ll never even have the chance to decide whether they’re better or not if you take the first thing that comes into your head and run with it.

-

The Case for Delayed Gratification

+

The Case for Delayed Gratification

Once upon a time, I set out to read Algorithms, by Robert Sedgewick (Addison-Wesley), which turned out to be a wonderful, stimulating, and most useful book, one that I recommend highly. My story, however, involves only what happened in the first 12 pages, for it was in those pages that Sedgewick discussed Euclid’s algorithm.

@@ -71,16 +74,20 @@

You see, I fell victim to a common programming pitfall, the “brute-force” syndrome. The basis of this syndrome is that there are many problems that have obvious, brute-force solutions—with one small drawback. The drawback is that if you were to try to apply a brute-force solution by hand—that is, work a single problem out with pencil and paper or a calculator—it would generally require that you have the patience and discipline to work on the problem for approximately seven hundred years, not counting eating and sleeping, in order to get an answer. Finding all the prime numbers less than 1,000,000 is a good example; just divide each number up to 1,000,000 by every lesser number, and see what’s left standing. For most of the history of humankind, people were forced to think of cleverer solutions, such as the Sieve of Eratosthenes (we’d have been in big trouble if the ancient Greeks had had computers), mainly because after about five minutes of brute force-type work, people’s attention gets diverted to other important matters, such as how far a paper airplane will fly from a second-story window.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/10-02.html b/10-02.html index 7044396..1e04d07 100644 --- a/10-02.html +++ b/10-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Not so nowadays, though. Computers love boring work; they’re very patient and disciplined, and, besides, one human year = seven dog years = two zillion computer years. So when we’re faced with a problem that has an obvious but exceedingly lengthy solution, we’re apt to say, “Ah, let the computer do that, it’s fast,” and go back to making paper airplanes. Unfortunately, brute-force solutions tend to be slow even when performed by modern-day microcomputers, which are capable of several MIPS except when I’m late for an appointment and want to finish a compile and run just one more test before I leave, in which case the crystal in my computer is apparently designed to automatically revert to 1 Hz.)

The solution that I instantly came up with to finding the GCD is about as brute- force as you can get: Divide both the larger integer (iL) and the smaller integer (iS) by every integer equal to or less than the smaller integer, until a number is found that divides both evenly, as shown in Figure 10.1. This works, but it’s a lousy solution, requiring as many as iS*2 divisions; very expensive, especially for large values of iS. For example, finding the GCD of 30,001 and 30,002 would require 60,002 divisions, which alone, disregarding tests and branches, would take about 2 seconds on an 8088, and more than 50 milliseconds even on a 25 MHz 486—a very long time in computer years, and not insignificant in human years either.

@@ -240,7 +243,7 @@ unsigned int gcd(unsigned int int1, unsigned int int2) { }
-

Wasted Breakthroughs

+

Wasted Breakthroughs

Sedgewick’s first solution to the GCD problem was pretty much the one I came up with. He then pointed out that the GCD of iL and iS is the same as the GCD of iL-iS and iS. This was obvious (once Sedgewick pointed it out); by the very nature of division, any number that divides iL evenly nL times and iS evenly nS times must divide iL-iS evenly nL-nS times. Given that insight, I immediately designed a new, faster approach, shown in Listing 10.2.

@@ -277,16 +280,20 @@ unsigned int gcd(unsigned int int1, unsigned int int2) { }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/10-03.html b/10-03.html index 6bf750b..698ecf1 100644 --- a/10-03.html +++ b/10-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 10.2 repeatedly subtracts iS from iL until iL becomes less than or equal to iS. If iL becomes equal to iS, then that’s the GCD; alternatively, if iL becomes less than iS, iL and iS switch values, and the process is repeated, as shown in Figure 10.2. The number of iterations this approach requires relative to Listing 10.1 depends heavily on the values of iL and iS, so it’s not always faster, but, as Table 10.1 indicates, Listing 10.2 is generally much better code.


@@ -141,20 +144,24 @@ unsigned int gcd(unsigned int int1, unsigned int int2) { }

-

Patient Optimization

+

Patient Optimization

At long last, we’re ready to optimize GCD determination in the classic sense. Table 10.1 shows the performance of Listing 10.4 with and without Microsoft C/C++’s maximum optimization, and also shows the performance of Listing 10.5, an assembly language version of Listing 10.4. Sure, the optimized versions are faster than the unoptimized version of Listing 10.4—but the gains are small compared to those realized from the higher-level optimizations in Listings 10.2 through 10.4.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/10-04.html b/10-04.html index 1a349d5..8d607cd 100644 --- a/10-04.html +++ b/10-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 10.5 L10-5.ASM

 ; Finds and returns the greatest common divisor of two integers.
@@ -139,16 +142,20 @@ _gcd  endp
 
   

And think what you could do with all those extra computer years!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/11-01.html b/11-01.html index c41abef..8f534ef 100644 --- a/11-01.html +++ b/11-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 11
Pushing the 286 and 386

@@ -39,7 +42,7 @@

This chapter provides an interesting look at the evolution of the x86 architecture, to a greater degree than you might expect, for the x86 family came into full maturity with the 386; the 486 and the Pentium are really nothing more than faster 386s, with very little in the way of new functionality. In contrast, the 286 added a number of instructions, respectable performance, and protected mode to the 8088’s capabilities, and the 386 added more instructions and a whole new set of addressing modes, and brought the x86 family into the 32-bit world that represents the future (and, increasingly, the present) of personal computing. This chapter also provides insight into the effects on optimization of the variations in processors and memory architectures that are common in the PC world. So, although the 286 and 386 no longer represent the mainstream of computing, this chapter is a useful mix of history lesson, x86 overview, and details on two workhorse processors that are still in wide use.

-

Family Matters

+

Family Matters

While the x86 family is a large one, only a few members of the family—which includes the 8088, 8086, 80188, 80186, 286, 386SX, 386DX, numerous permutations of the 486, and now the Pentium—really matter.

@@ -53,7 +56,7 @@

This leaves us with just two processors: the 286 and the 386. Each was the PC standard in its day. The 286 is no longer used in new systems, but there are millions of 286-based systems still in daily use. The 386 is still being used in new systems, although it’s on the downhill leg of its lifespan, and it is in even wider use than the 286. The future clearly belongs to the 486 and Pentium, but the 286 and 386 are still very much a part of the present-day landscape.

-

Crossing the Gulf to the 286 and the 386

+

Crossing the Gulf to the 286 and the 386

Apart from vastly improved performance, the biggest difference between the 8088 and the 286 and 386 (as well as the later Intel CPUs) is that the 286 introduced protected mode, and the 386 greatly expanded the capabilities of protected mode. We’re only going to talk about real-mode operation of the 286 and 386 in this book, however. Protected mode offers a whole new memory management scheme, one that isn’t supported by the 8088. Only code specifically written for protected mode can run in that mode; it’s an alien and hostile environment for MS-DOS programs.

@@ -75,22 +78,26 @@ mov dx,word ptr [LongVar+2]

In short, taken as a whole, protected mode programming is a different kettle of fish altogether from what I’ve been describing in this book. There’s certainly a knack to optimizing specifically for protected mode under a given operating system...but it’s not what we’ve been learning, and now is not the time to pursue it further. In general, though, the optimization strategies discussed in this book still hold true in protected mode; it’s just issues specific to protected mode or a particular operating system that we won’t discuss.

-

In the Lair of the Cycle-Eaters, Part II

+

In the Lair of the Cycle-Eaters, Part II

Under the programming interface, the 286 and 386 differ considerably from the 8088. Nonetheless, with one exception and one addition, the cycle-eaters remain much the same on computers built around the 286 and 386. Next, we’ll review each of the familiar cycle-eaters I covered in Chapter 4 as they apply to the 286 and 386, and we’ll look at the new member of the gang, the data alignment cycle-eater.

The one cycle-eater that vanishes on the 286 and 386 is the 8-bit bus cycle-eater. The 286 is a 16-bit processor both internally and externally, and the 386 is a 32-bit processor both internally and externally, so the Execution Unit/Bus Interface Unit size mismatch that plagues the 8088 is eliminated. Consequently, there’s no longer any need to use byte-sized memory variables in preference to word-sized variables, at least so long as word-sized variables start at even addresses, as we’ll see shortly. On the other hand, access to byte-sized variables still isn’t any slower than access to word-sized variables, so you can use whichever size suits a given task best.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/11-02.html b/11-02.html index d7eb15e..246c98d 100644 --- a/11-02.html +++ b/11-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

You might think that the elimination of the 8-bit bus cycle-eater would mean that the prefetch queue cycle-eater would also vanish, since on the 8088 the prefetch queue cycle-eater is a side effect of the 8-bit bus. That would seem all the more likely given that both the 286 and the 386 have larger prefetch queues than the 8088 (6 bytes for the 286, 16 bytes for the 386) and can perform memory accesses, including instruction fetches, in far fewer cycles than the 8088.

However, the prefetch queue cycle-eater doesn’t vanish on either the 286 or the 386, for several reasons. For one thing, branching instructions still empty the prefetch queue, so instruction fetching still slows things down after most branches; when the prefetch queue is empty, it doesn’t much matter how big it is. (Even apart from emptying the prefetch queue, branches aren’t particularly fast on the 286 or the 386, at a minimum of seven-plus cycles apiece. Avoid branching whenever possible.)

@@ -89,16 +92,20 @@ Skip:

What does this mean? It means that, practically speaking, the 286 as used in the AT doesn’t have a 16-bit bus. From a performance perspective, the 286 in an AT has two-thirds of a 16-bit bus (a 10.7-bit bus?), since every bus access on an AT takes 50 percent longer than it should. A 286 running at 10 MHz should be able to access memory at a maximum rate of 1 word every 200 ns; in a 10 MHz AT, however, that rate is reduced to 1 word every 300 ns by the one-wait-state memory.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/11-03.html b/11-03.html index c6eefaf..eb94d5e 100644 --- a/11-03.html +++ b/11-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

In short, a close relative of our old friend the 8-bit bus cycle-eater—the system memory wait state cycle-eater—haunts us still on all but zero-wait-state 286 and 386 computers, and that means that the prefetch queue cycle-eater is alive and well. (The system memory wait state cycle-eater isn’t really a new cycle-eater, but rather a variant of the general wait state cycle-eater, of which the display adapter cycle-eater is yet another variant.) While the 286 in the AT can fetch instructions much faster than can the 8088 in the PC, it can execute those instructions faster still.

The picture is less clear in the 386 world since there are so many different memory architectures, but similar problems can occur in any computer built around a 286 or 386. The prefetch queue cycle-eater is even a factor—albeit a lesser one—on zero-wait-state machines, both because branching empties the queue and because some instructions can outrun even zero—5 cycles longer than the official execution time.)

@@ -128,22 +131,26 @@ Skip:

The data alignment cycle-eater has intriguing implications for speeding up 286/386 code. The expenditure of a little care and a few bytes to make sure that word-sized variables and memory blocks are word-aligned can literally double the performance of certain code running on the 286. Even if it doesn’t double performance, word alignment usually helps and never hurts.

-

Code Alignment

+

Code Alignment

Lack of word alignment can also interfere with instruction fetching on the 286, although not to the extent that it interferes with access to word-sized memory variables. The 286 prefetches instructions a word at a time; even if a given instruction doesn’t begin at an even address, the 286 simply fetches the first byte of that instruction at the same time that it fetches the last byte of the previous instruction, as shown in Figure 11.2, then separates the bytes internally. That means that in most cases, instructions run just as fast whether they’re word-aligned or not.

There is, however, a non-word-alignment penalty on branches to odd addresses. On a branch to an odd address, the 286 is only able to fetch 1 useful byte with the first instruction fetch following the branch, as shown in Figure 11.3. In other words, lack of word alignment of the target instruction for any branch effectively cuts the instruction-fetching power of the 286 in half for the first instruction fetch after that branch. While that may not sound like much, you’d be surprised at what it can do to tight loops; in fact, a brief story is in order.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/11-04.html b/11-04.html index 2df2e9f..23ef4cf 100644 --- a/11-04.html +++ b/11-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

When I was developing the Zen timer, I used my trusty 10 MHz 286-based AT clone to verify the basic functionality of the timer by measuring the performance of simple instruction sequences. I was cruising along with no problems until I timed the following code:

 
@@ -115,16 +118,20 @@ FindChar  proc near
 
   

The two ways of looking at the display adapter cycle-eater on 286/386 computers are actually the same. As you’ll recall from my earlier discussion of the matter in Chapter 4, display adapters offer only a limited number of accesses to display memory during any given period of time. The 8088 is capable of making use of most but not all of those slots with REP MOVSW, so the number of memory accesses allowed by a display adapter such as a standard VGA is reasonably well-matched to an 8088’s memory access speed. Granted, access to a VGA slows the 8088 down considerably—but, as we’re about to find out, “considerably” is a relative term. What a VGA does to PC performance is nothing compared to what it does to faster computers.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/11-05.html b/11-05.html index 1b66ca3..dcacf4b 100644 --- a/11-05.html +++ b/11-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Under ideal conditions, a 286 can access memory much, much faster than an 8088. A 10 MHz 286 is capable of accessing a word of system memory every 0.20 ms with REP MOVSW, dwarfing the 1 byte every 1.31 µs that the 8088 in a PC can manage. However, access to display memory is anything but ideal for a 286. For one thing, most display adapters are 8-bit devices, although newer adapters are 16-bit in nature. One consequence of that is that only 1 byte can be read or written per access to display memory; word-sized accesses to 8-bit devices are automatically split into 2 separate byte-sized accesses by the AT’s bus. Another consequence is that accesses are simply slower; the AT’s bus inserts additional wait states on accesses to 8-bit devices since it must assume that such devices were designed for PCs and may not run reliably at AT speeds.

However, the 8-bit size of most display adapters is but one of the two factors that reduce the speed with which the 286 can access display memory. Far more cycles are eaten by the inherent memory-access limitations of display adapters—that is, the limited number of display memory accesses that display adapters make available to the 286. Look at it this way: If REP MOVSW on a PC can use more than half of all available accesses to display memory, then how much faster can code running on a 286 or 386 possibly run when accessing display memory?

@@ -54,7 +57,7 @@

What can we do about this new, more virulent form of the display adapter cycle-eater? The workaround is the same as it was on the PC: Access display memory as little as you possibly can.

-

New Instructions and Features: The 286

+

New Instructions and Features: The 286

The 286 and 386 offer a number of new instructions. The 286 has a relatively small number of instructions that the 8088 lacks, while the 386 has those instructions and quite a few more, along with new addressing modes and data sizes. We’ll discuss the 286 and the 386 separately in this regard.

@@ -64,7 +67,7 @@

A couple of old instructions gain new features on the 286. For one, the 286 version of PUSH is capable of pushing a constant on the stack. For another, the 286 allows all shifts and rotates to be performed for not just 1 bit or the number of bits specified by CL, but for any constant number of bits.

-

New Instructions and Features: The 386

+

New Instructions and Features: The 386

The 386 is somewhat more complex than the 286 regarding new features. Once again, we won’t discuss protected mode, which on the 386 comes with the ability to address up to 4 gigabytes per segment and 64 terabytes in all. In real mode (and in virtual-86 mode, which allows the 386 to multitask MS-DOS applications, and which is identical to real mode so far as MS-DOS programs are concerned), programs running on the 386 are still limited to 1 MB of addressable memory and 64K per segment.

@@ -74,16 +77,20 @@

The 386 also comes with a slew of new real-mode instructions beyond those supported by the 8088 and 286. These instructions can scan data on a bit-by-bit basis, set the Carry flag to the value of a specified bit, sign-extend or zero-extend data as it’s moved, set a register or memory variable to 1 or 0 on the basis of any of the conditions that can be tested with conditional jumps, and more. (Again, beware: Many of these complex 386-specific instructions are slower than equivalent sequences of simple instructions on the 486 and especially on the Pentium.) What’s more, both old and new instructions support 32-bit operations on the 386. For example, it’s relatively simple to copy data in chunks of 4 bytes on a 386, even in real mode, by using the MOVSD (“move string double”) instruction, or to negate a 32-bit value with NEG eax.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/11-06.html b/11-06.html index 8640755..ad1d004 100644 --- a/11-06.html +++ b/11-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Finally, it’s possible in real mode to use the 386’s new addressing modes, in which any 32-bit general-purpose register or pair of registers can be used to address memory. What’s more, multiplication of memory-addressing registers by 2, 4, or 8 for look-ups in word, doubleword, or quadword tables can be built right into the memory addressing mode. (The 32-bit addressing modes are discussed further in later chapters.) In protected mode, these new addressing modes allow you to address a full 4 gigabytes per segment, but in real mode you’re still limited to 64K, even with 32-bit registers and the new addressing modes, unless you play some unorthodox tricks with the segment registers.

@@ -80,16 +83,20 @@ call ZTimerOff -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/11-07.html b/11-07.html index 885c180..cd16ae9 100644 --- a/11-07.html +++ b/11-07.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 11.5 L11-5.ASM

 ;
@@ -61,7 +64,7 @@ Skip:
 
   

The more things change, the more they remain the same....

-

POPF and the 286

+

POPF and the 286

We’ve one final 286-related item to discuss: the hardware malfunction of POPF under certain circumstances on the 286.

@@ -73,16 +76,20 @@ Skip:

All POPF does is pop the word on top of the stack into the FLAGS register, as shown in Figure 11.4. How can we do that without POPF? Of course, the 286’s designers intended us to use POPF for this purpose, and didn’t intentionally provide any alternative approach, so we’ll have to devise an alternative approach of our own. To do that, we’ll have to search for instructions that contain some of the same functionality as POPF, in the hope that one of those instructions can be used in some way to replace POPF.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/11-08.html b/11-08.html index f22716a..6e4e6c8 100644 --- a/11-08.html +++ b/11-08.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Well, there’s only one instruction other than POPF that loads the FLAGS register directly from the stack, and that’s IRET, which loads the FLAGS register from the stack as it branches, as shown in Figure 11.5. iret has no known bugs of the sort that plague POPF, so it’s certainly a candidate to replace popf in non-interruptible applications. Unfortunately, IRET loads the FLAGS register with the third word down on the stack, not the word on top of the stack, as is the case with POPF; the far return address that IRET pops into CS:IP lies between the top of the stack and the word popped into the FLAGS register.

Obviously, the segment:offset that IRET expects to find on the stack above the pushed flags isn’t present when the stack is set up for POPF, so we’ll have to adjust the stack a bit before we can substitute IRET for POPF. What we’ll have to do is push the segment:offset of the instruction after our workaround code onto the stack right above the pushed flags. IRET will then branch to that address and pop the flags, ending up at the instruction after the workaround code with the flags popped. That’s just the result that would have occurred had we executed POPF—WITH the bonus that no interrupts can accidentally occur when the Interrupt flag is 0 both before and after the pop.

@@ -103,16 +106,20 @@ EMULATE_POPFmacro

And now you know the nature of and the workaround for the POPF bug. Whether you ever need the workaround or not, it’s a neatly packaged example of the tremendous flexibility of the x86 instruction set.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/12-01.html b/12-01.html index 506541a..2297eb3 100644 --- a/12-01.html +++ b/12-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 12
Pushing the 486

@@ -45,7 +48,7 @@

Substitute “processor” for the various digging implements, and you get an idea of just how different the optimization rules for the 486 are from what you’re used to. Okay, it’s not quite that bad—but upon encountering a processor where string instructions are often to be avoided and memory-to-register MOVs are frequently as fast as register-to-register MOVs, Dorothy was heard to exclaim (before she sank out of sight in a swirl of hopelessly mixed metaphors), “I don’t think we’re in Kansas anymore, Toto.”

-

Enter the 486

+

Enter the 486

No chip that is a direct, fully compatible descendant of the 8088, 286, and 386 could ever be called a RISC chip, but the 486 certainly contains RISC elements, and it’s those elements that are most responsible for making 486 optimization unique. Simple, common instructions are executed in a single cycle by a RISC-like core processor, but other instructions are executed pretty much as they were on the 386, where every instruction takes at least 2 cycles. For example, MOV AL, [TestChar] takes only 1 cycle on the 486, assuming both instruction and data are in the cache—3 cycles faster than the 386—but STOSB takes 5 cycles, 1 cycle slower than on the 386. The floating-point execution unit inside the 486 is also much faster than the 387 math coprocessor, largely because, being in the same silicon as the CPU (the 486 has a math coprocessor built in), it is more tightly coupled. The results are sometimes startling: FMUL (floating point multiply) is usually faster on the 486 than IMUL (integer multiply)!

@@ -65,7 +68,7 @@

In other words, for cached code (which time-critical code almost always is), performance is predictable and can be calculated with good precision, and those calculations will apply on any 486. However, “predictable” doesn’t mean “trivial”; the cycle times printed for the various instructions are not the whole story. You must be aware of all the rules, documented and undocumented, that go into calculating actual execution times—and uncovering some of those rules is exactly what this chapter is about.

-

The Hazards of Indexed Addressing

+

The Hazards of Indexed Addressing

Rule #1: Avoid indexed addressing (that is, try not to use either two registers or scaled addressing to point to memory).

@@ -101,16 +104,20 @@ LoopTop: sub si,bx
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/12-02.html b/12-02.html index 96449a6..56fad21 100644 --- a/12-02.html +++ b/12-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

which calculates the same sum and leaves the registers in the same state as the first example, but avoids indexed addressing.

In protected mode, the definition of indexed addressing is a tad more complex. The use of two registers to address memory, as in MOV EAX, [EDX+EDI], still qualifies for the one-cycle penalty. In addition, the use of 386/486 scaled addressing, as in MOV [ECX*2],EAX, also constitutes indexed addressing, even if only one register is used to point to memory.

@@ -40,7 +43,7 @@

In a key loop on the 486, 1 cycle can indeed matter.

-

Calculate Memory Pointers Ahead of Time

+

Calculate Memory Pointers Ahead of Time

Rule #2: Don’t use a register as a memory pointer during the next two cycles after loading it.

@@ -125,16 +128,20 @@ jnz LoopTop

A caution: I’m quite certain that the 2-cycle-ahead addressing pipeline interruption penalty I’ve described exists in the two 486s I’ve tested. However, there’s no guarantee that Intel won’t change this aspect of the 486 in the future, especially given that the documentation indicates otherwise. Perhaps the 2-cycle penalty is the result of a bug in the initial steps of the 486, and will revert to the documented 1-cycle penalty someday; likewise for the undocumented optimizations I’ll describe below. Nonetheless, none of the optimizations I suggest would hurt performance even if the undocumented performance characteristics of the 486 were to vanish, and they certainly will help performance on at least some 486s right now, so I feel they’re well worth using.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/12-03.html b/12-03.html index 53b5db0..e06c9d8 100644 --- a/12-03.html +++ b/12-03.html @@ -1,5 +1,4 @@ - + @@ -19,20 +18,24 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

There is, of course, no guarantee that I’m entirely correct about the optimizations discussed in this chapter. Without knowing the internals of the 486, all I can do is time code and make inferences from the results; I invite you to deduce your own rules and cross-check them against mine. Also, most likely there are other optimizations that I’m unaware of. If you have further information on these or any other undocumented optimizations, please write and let me know. And, of course, if anyone from Intel is reading this and wants to give us the gospel truth, please do!

-

Stack Addressing and Address Pipelining

+

Stack Addressing and Address Pipelining

Rule #2A: Rule #2 sometimes, but not always, applies to the stack pointer when it is implicitly used to point to memory.

@@ -74,7 +77,7 @@ pop ax

I certainly haven’t tried all possible combinations, but the results so far indicate that the stack pointer incurs the addressing pipeline penalty only if (E)SP is the explicit destination of one instruction and is then used by one of the two following instructions to address memory. So, for instance, SP isn’t the explicit operand of POP AX—AX is—and no cycles are lost if POP AX is followed by POP or RET. Happily, then, we need not worry about the sequence in which we use PUSH and POP. However, adding to, moving to, or subtracting from the stack pointer should ideally be done at least two cycles before PUSH, POP, RET, or any other instruction that uses the stack pointer to address memory.

-

Problems with Byte Registers

+

Problems with Byte Registers

There are two ways to lose cycles by using byte registers, and neither of them is documented by Intel, so far as I know. Let’s start with the lesser and simpler of the two.

@@ -120,16 +123,20 @@ xlat

In general, penalties for interrupting the 486’s pipeline apply primarily to the fast core instructions of the 486, most notably register-only instructions and MOV, although arithmetic and logical operations that access memory are also often affected. I don’t know all the performance dependencies, and I don’t plan to; figuring all of them out would be a big, boring job of little value. Basically, on the 486 you should concentrate on using those fast core instructions when performance matters, and all the rules I’ll discuss do indeed apply to those instructions.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/12-04.html b/12-04.html index a3d7d8f..99590a4 100644 --- a/12-04.html +++ b/12-04.html @@ -1,5 +1,4 @@ - + @@ -19,20 +18,24 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

You don’t need to understand every corner of the 486 universe unless you’re a diehard ASMhead who does this stuff for fun. Just learn enough to be able to speed up the key portions of your programs, and spend the rest of your time on a fast design and overall implementation.

-

More Fun with Byte Registers

+

More Fun with Byte Registers

Rule #4: Don’t load any byte register exactly 2 cycles before using any register to address memory.

@@ -78,7 +81,7 @@ mov ax,[bx] -

Timing Your Own 486 Code

+

Timing Your Own 486 Code

In case you want to do some 486 performance analysis of your own, let me show you how I arrived at one of the above conclusions; at the same time, I can warn you of the timing hazards of the cache. Listings 12.1 and 12.2 show the code I ran through the Zen timer in order to establish the effects of loading a byte register before using a register to address memory. Listing 12.1 ran in 120 µs on a 33 MHz 486, or 4 cycles per repetition (120 µs/1000 repetitions = 120 ns per repetition; 120 ns per repetition/30 ns per cycle = 4 cycles per repetition); Listing 12.2 ran in 90 µs, or 3 cycles, establishing that loading a byte register costs a cycle only when it’s performed exactly 2 cycles before addressing memory.

@@ -140,16 +143,20 @@ Done:

Sometimes it is hard to believe we’re still in Kansas!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/13-01.html b/13-01.html index 9fab9fc..dec14b0 100644 --- a/13-01.html +++ b/13-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 13
Aiming the 486

@@ -43,7 +46,7 @@

For example, consider how Terje Mathisen doubled the speed of his word-counting program on a 486 simply by shuffling a couple of instructions.

-

486 Pipeline Optimization

+

486 Pipeline Optimization

I’ve mentioned Terje Mathisen in my writings before. Terje is an assembly language programmer extraordinaire, and author of the incredibly fast public-domain word-counting program WC (which comes complete with source code; well worth a look, if you want to see what really fast code looks like). Terje’s a regular participant in the ibm.pc/fast.code topic on Bix. In a thread titled “486 Pipeline Optimization, or TANSTATFC (There Ain’t No Such Thing As The Fastest Code),” he detailed the following optimization to WC, perhaps the best example of 486 pipeline optimization I’ve yet seen.

@@ -82,16 +85,20 @@ add dx,[bx+8000h] ;increment word and line count

At this point, Terje had nearly doubled the performance of this code simply by moving one instruction. (Note that swapping the instructions also made it necessary to preload DI at the start of the loop; Listing 13.2 is not exactly equivalent to Listing 13.1.) I’ll let Terje describe his next optimization in his own words:

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/13-02.html b/13-02.html index 33aa119..5d80c49 100644 --- a/13-02.html +++ b/13-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

“When I looked closely as this, I realized that the two cycles for the final ADD is just the sum of 1 cycle to load the data from memory, and 1 cycle to add it to DX, so the code could just as well have been written as shown in Listing 13.3. The final breakthrough came when I realized that by initializing AX to zero outside the loop, I could rearrange it as shown in Listing 13.4 and do the final ADD DX,AX after the loop. This way there are two single-cycle instructions between the first and the fourth line, avoiding all pipeline stalls, for a total throughput of two cycles/char.”

LISTING 13.3 L13-3.ASM

@@ -88,16 +91,20 @@ looptop:

Not necessarily. Shifts and rotates are among the worst performing instructions of the 486, taking 2 to 3 cycles to execute. Thus, it takes 2 cycles to rotate the skip value into CX in Listing 13.5, and 2 more cycles to rotate it back to the upper half of ECX. I’d say four cycles is a pretty steep price to pay, especially considering that a MOV to or from memory takes only one cycle. Basically, using ROR to access a 16-bit value in the upper half of a 16-bit register is a pretty marginal technique, unless for some reason you can’t access memory at all (for example, if you’re using BP as a working register, temporarily making the stack frame inaccessible).

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/13-03.html b/13-03.html index a383085..5e3e56e 100644 --- a/13-03.html +++ b/13-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

On the 386, ROR was the only way to split a 32-bit register into two 16-bit registers. On the 486, however, BSWAP can not only do the job, but can do it better, because BSWAP executes in just one cycle. BSWAP has the added benefit of not affecting any flags, unlike ROR. With BSWAP-based code like that in Listing 13.6, the upper 16 bits of a register can be accessed with only 2 cycles of overhead and without altering any flags, making the technique of packing two 16-bit registers into one 32-bit register much more useful.

LISTING 13.6 L13-6.ASM

@@ -89,16 +92,20 @@ mov dx,ax

Again, this technique is advantageous only on a 486. It also doesn’t apply to RCL and RCR, where you definitely want to use the 1-bit versions whenever you can, because the n-bit versions are horrendously slow. But if you’re optimizing for the 486, these tidbits can save a few critical cycles—and Lord knows that if you’re optimizing for the 486—that is, if you need even more performance than you get from unoptimized code on a 486—you almost certainly need all the speed you can get.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/13-04.html b/13-04.html index 093a597..b6b9f87 100644 --- a/13-04.html +++ b/13-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

32-Bit Addressing Modes

The 386 and 486 both support 32-bit addressing modes, in which any register may serve as the base memory addressing register, and almost any register may serve as the potentially scaled index register. For example,

@@ -77,16 +80,20 @@ LoopTop:

Lastly, as I mentioned, ESP cannot be scaled. In fact, ESP cannot be an index register; it must be a base register. Ironically, however, ESP is the one register that cannot be used to address memory without the presence of an SIB byte, even if it’s used without an index register. This is an outcome of the way in which the SIB byte extends the capabilities of the Mod-R/M byte, and there’s nothing to be done about it, but it’s at least worth noting that ESP-based, non-indexed addressing makes for instructions that are a byte larger than other non-indexed addressing (but not any slower; there’s no 1-cycle penalty for using ESP as a base register) on the 486.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/14-01.html b/14-01.html index 15ac494..45c7d4d 100644 --- a/14-01.html +++ b/14-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 14
Boyer-Moore String Searching

@@ -77,16 +80,20 @@

Actually, yes, we can.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/14-02.html b/14-02.html index 9ad7519..565944c 100644 --- a/14-02.html +++ b/14-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The Boyer-Moore Algorithm

All our a priori knowledge of string searching is stated above, but there’s another sort of knowledge—knowledge that’s generated dynamically. As we search through the buffer, we acquire information each time we check for a match. One sort of information that we acquire is based on partial matches; we can often skip ahead after partial matches because (take a deep breath!) by partially matching, we have already implicitly done a comparison of the partially matched buffer characters with all possible pattern start locations that overlap those partially-matched bytes.

@@ -71,16 +74,20 @@

The best case for Boyer-Moore is good indeed: About N/M comparisons are required, where N is the buffer length and M is the pattern length. This reflects the ability of Boyer-Moore to skip ahead by a full pattern length on a complete mismatch.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/14-03.html b/14-03.html index 0ec8d4e..d612109 100644 --- a/14-03.html +++ b/14-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

How fast is Boyer-Moore? Listing 14.1 is a C implementation of Boyer-Moore searching; Listing 14.2 is a test-bed program that searches up to the first 32K of a file for a pattern. Table 14.1 (all times measured with Turbo Profiler on a 20 MHz cached 386, searching a modified version of the text of this chapter) shows that this implementation is generally much slower than REPNZ SCASB, although it does come close when searching for long patterns. Listing 14.1 is designed primarily to make later assembly implemenmore comprehensible, rather than faster; Sedge’s implementation uses arrays rather than pointers, is a great deal more compact and very clever, and may be somewhat faster. Regardless, the far superior performance of REPNZ SCASB clearly indicates that assembly language is in order at this point.

@@ -197,16 +200,20 @@

The entry “Standard Boyer-Moore in ASM” in Table 14.1 refers to straight-forward hand optimization of Listing 14.1, code that is not included in this chapter for the perfectly good reason that it is slower in most cases than REPNZ SCASB. I say this casually now, but not so yesterday, when I had all but concluded that Boyer-Moore was simply inferior on the x86, due to two architectural quirks: the string instructions and slow branch. I had even coined a neat phrase for it: Architecture is destiny. Has a nice ring, doesn’t it?

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/14-04.html b/14-04.html index f83e2af..112b6ce 100644 --- a/14-04.html +++ b/14-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 14.1 L14-1.C

 /* Searches a buffer for a specified pattern. In case of a mismatch,
@@ -179,16 +182,20 @@ void main() {
 
   

Well, architecture carries a lot of weight, but it sure as heck isn’t destiny. I had simply fallen into the trap of figuring that the algorithm was so clever that I didn’t have to do any thinking myself. The path leading to REPNZ SCASB from the original brute-force approach of REPZ CMPSB at every location had been based on my observation that the first character comparison at each buffer location usually fails. Why not apply the same concept to Boyer-Moore? Listing 14.3 is just like the standard implementation—except that it’s optimized to handle a first-comparison mismatch as quickly as possible in the loop at QuickSearchLoop, much as REPNZ SCASB optimizes first-comparison mismatches for the brute-force approach. The results in Table 14.1 speak for themselves; Listing 14.3 is more than twice as fast as what I assure you was already a nice, tight assembly implementation (and unrolling QuickSearchLoop could boost performance by up to 10 percent more). Listing 14.3 is also four times faster than REPNZ SCASB in one case.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/14-05.html b/14-05.html index faf4b1a..f24ece0 100644 --- a/14-05.html +++ b/14-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 14.3 L14-3.ASM

 ; Searches a buffer for a specified pattern. In case of a mismatch,
@@ -194,16 +197,20 @@ _FindString     endp
         end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/14-06.html b/14-06.html index 2e6afda..2a0992a 100644 --- a/14-06.html +++ b/14-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Table 14.1 represents a limited and decidedly unscientific comparison of searching techniques. Nonetheless, the overall trend is clear: For all but the shortest patterns, well-implemented Boyer-Moore is generally as good as or better than—sometimes much better than—brute-force searching. (For short patterns, you might want to use REPNZ SCASB, thereby getting the best of both worlds.)

Know your data and use your smarts. Don’t stop thinking just because you’re implementing a big-name algorithm; you know more than it does.

@@ -196,16 +199,20 @@ _FindString endp

As Yogi Berra might put it, “You don’t know what you know until you know it.”

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/15-01.html b/15-01.html index d1e966c..3223f85 100644 --- a/15-01.html +++ b/15-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 15
Linked Lists and plain Unintended Challenges

@@ -70,16 +73,20 @@


Figure 15.1
  The basic concept of a linked list.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/15-02.html b/15-02.html index 5e58906..bb708ec 100644 --- a/15-02.html +++ b/15-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 15.1 L15-1.C

 /* Deletes the node in a linked list that follows the indicated node.
@@ -143,16 +146,20 @@ struct LinkNode *FindNodeBeforeValueNotLess(
   


Figure 15.3
  Representing an empty list.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/15-03.html b/15-03.html index ee74fef..3b62064 100644 --- a/15-03.html +++ b/15-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 15.5 L15-5.C

 /* Finds the first node in a value-sorted linked list that
@@ -142,16 +145,20 @@ struct LinkNode *InsertNodeSorted(struct LinkNode *HeadOfListNode,
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/15-04.html b/15-04.html index 4b5c24e..e8b15dd 100644 --- a/15-04.html +++ b/15-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 15.7 L15-7.ASM

 ; C near-callable assembly function for inserting a new node in a
@@ -221,16 +224,20 @@ around:         ja      save
 
   

Before I end this chapter, let me say that I get a lot of feedback from my readers, and it’s much appreciated. Keep those cards, letters, and email messages coming. And if any of you know Jeannie Schweigert, have her drop me a line and let me know how she’s doing these days....

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/16-01.html b/16-01.html index 3ae504e..3cbea74 100644 --- a/16-01.html +++ b/16-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 16
There Ain’t No Such Thing as the Fastest Code

@@ -198,16 +201,20 @@
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/16-02.html b/16-02.html index abbba4b..5cda1a4 100644 --- a/16-02.html +++ b/16-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 16.2 is Listing 16.1 modified to call a function that scans each block for words, and Listing 16.3 contains an assembly function that counts words. Used together, Listings 16.2 and 16.3 are just about twice as fast as Listing 16.1, a good return for a little assembly language. Listing 16.3 is a pretty straightforward translation from C to assembly; the new code makes good use of registers, but the key code—determining whether each byte is a character or not—is still done with the same multiple-sequential-tests approach used by the code that the C compiler generates.

LISTING 16.2 L16-2.C

@@ -178,20 +181,24 @@ end
-

Which Way to Go from Here?

+

Which Way to Go from Here?

We could rearrange the tests in light of the nature of the data being scanned; for example, we could perform the tests more efficiently by taking advantage of the knowledge that if a byte is less than ‘0,’ it’s either an apostrophe or not a character at all. However, that sort of fine-tuning is typically good for speedups of only 10 to 20 percent, and I’ve intentionally refrained from implementing this in Listing 16.3 to avoid pointing you down the wrong path; what we need is a different tack altogether. Ponder this. What we really want to know is nothing more than whether a byte is a character, not what sort of character it is. For each byte value, we want a yes/no status, and nothing else—and that description practically begs for a lookup table. Listing 16.4 uses a lookup table approach to boost performance another 50 percent, to three times the performance of the original C code. On a 20 MHz 386, this represents a change from 4.6 to 1.6 seconds, which could be significant—who likes to wait? On an 8088, the improvement in word-counting a large file could easily be 10 or 20 seconds, which is definitely significant.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/16-03.html b/16-03.html index 4807d1a..4d4a938 100644 --- a/16-03.html +++ b/16-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 16.4 L16-4.ASM

  ; Assembly subroutine for Listing 16.2. Scans through Buffer, of
@@ -147,16 +150,20 @@
 
   

So how did the entrants in this particular challenge stack up? More than one claimed a speed-up over my assembly word-counting code of more than three times. On top of the three-times speedup over the original C code that I had already realized, we’re almost up to an order of magnitude faster. You are, of course, entitled to your own opinion, but I consider an order of magnitude to be significant.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/16-04.html b/16-04.html index 3a12c77..22927b3 100644 --- a/16-04.html +++ b/16-04.html @@ -1,5 +1,4 @@ - + @@ -19,20 +18,24 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Truth to tell, I didn’t expect a three-times speedup; around two times was what I had in mind. Which just goes to show that any code can be made faster than you’d expect, if you think about it long enough and from many different perspectives. (The most potent word-counting technique seems to be a 64K lookup table that allows handling two bytes simultaneously. This is not the sort of technique one comes up with by brute-force optimization.) Thinking (or, worse yet, boasting) that your code is the fastest possible is rollescating on a tightrope in a hurricane; you’re due for a fall, if you catch my drift. Case in point: Terje Mathisen’s word-counting program.

-

Blinding Yourself to a Better Approach

+

Blinding Yourself to a Better Approach

Not so long ago, Terje Mathisen, who I introduced earlier in this book, wrote a very fast word-counting program, and posted it on Bix. When I say it was fast, I mean fast; this code was optimized like nobody’s business. We’re talking top-quality code here.

@@ -57,7 +60,7 @@

(Granted, CMP [mem],reg is 1 cycle slower than CMP reg,[mem] on the 286, and they’re both the same on the 8088; in this case, though, the code was specific to the 386. In case you’re curious, both forms take 2 cycles on the 486; quite a lot faster, eh?)

-

Watch Out for Luggable Assumptions!

+

Watch Out for Luggable Assumptions!

The first lesson to be learned here is not to lug assumptions that may no longer be valid from the 8088/286 world into the wonderful new world of 386 native-mode programming. The second lesson is that after you’ve slaved over your code for a while, you’re in no shape to see its flaws, or to be able to get the new perspectives needed to speed it up. I’ll bet Terje looked at that [EBX+EAX] addressing a hundred times while trying to speed up his code, but he didn’t really see what it did; instead, he saw what it was supposed to do. Mental shortcuts like this are what enable us to deal with the complexities of assembly language without overloading after about 20 instructions, but they can be a major problem when looking over familiar code.

@@ -81,16 +84,20 @@

The winner was David Stafford, who at the time was working for Borland International; his entry is shown in Listing 16.5. Dave Methvin, whom some of you may recall as a tech editor of the late, lamented PC Tech Journal, was a close second, and Mick Brown, about whom I know nothing more than that he is obviously an extremely good assembly language programmer, was a close third, as shown in Table 16.2, which precedes Listing 16.5. Those three were out ahead of the pack; the fourth-place entry, good as it was (twice as fast as my original code), was twice as slow as David’s winning entry, so you can see that David, Dave, and Mick attained a rarefied level of optimization indeed.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/16-05.html b/16-05.html index 1a70035..ded73b3 100644 --- a/16-05.html +++ b/16-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Table 16.2 has two times for each entry listed: the first value is the overall counting time, including time spent in the main program, disk I/O, and everything else; the second value is the time actually spent counting words, the time spent in ScanBuffer . The first value is the time perceived by the user, but the second value best reflects the quality of the optimization in each entry, since the rest of the overall execution time is fixed.

@@ -340,16 +343,20 @@ jumping. end -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/16-06.html b/16-06.html index 57ca1a6..9612948 100644 --- a/16-06.html +++ b/16-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Levels of Optimization

Three levels of optimization were evident in the word-counting entries I received in response to my challenge. I’d briefly describe them as “fine-tuning,” “new perspective,” and “table-driven state machine.” The latter categories produce faster code, but, by the same token, they are harder to design, harder to implement, and more difficult to understand, so they’re suitable for only the most demanding applications. (Heck, I don’t even guarantee that David Stafford’s entry works perfectly, although, knowing him, it probably does; the more complex and cryptic the code, the greater the chance for obscure bugs.)

@@ -42,7 +45,7 @@ -

Optimization Level 1: Good Code

+

Optimization Level 1: Good Code

The first level of optimization involves fine-tuning and clever use of the instruction set. The basic framework is still the same as my code (which in turn is basically the same as that of the original C code), but that framework is implemented more efficiently.

@@ -56,16 +59,20 @@

Listing 16.6, contributed by Willem Clements, of Granada, Spain, illustrates a variety of level 1 optimizations: the two-loop approach, the use of a 16- rather than 32-bit counter, and the use of LODSW . Together, these optimizations made Willem’s code nearly twice as fast as mine in Listing 16.4. A few details could stand improvement; for example, AND AX,AX is a shorter way to test for zero than CMP AX,0 , and ALIGN 2 could be used. Nonetheless, this is good code, and it’s also fairly compact and reasonably easy to understand. In short, this is an excellent example of how an hour or so of hand-optimization might accomplish significantly improved performance at a reasonable cost in complexity and time. This level of optimization is adequate for most purposes (and, in truth, is beyond the abilities of most programmers).

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/16-07.html b/16-07.html index 62f386e..5f6edbc 100644 --- a/16-07.html +++ b/16-07.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 16.6 OPT2.ASM

  ;
@@ -152,16 +155,20 @@
 
   

“My next shot was to get rid of all the branches in the loop. To do that, I reached back to my college hardware courses. I noticed that we were really looking at an edge triggered device we want to count each time the I’m a character state goes from one to zero. Remembering that XOR on two single-bit values will always return whether the bits are different or the same, I implemented a transition counter. The counter triggers every time a word begins or ends.”

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/16-08.html b/16-08.html index e793435..8b509d0 100644 --- a/16-08.html +++ b/16-08.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 16.7 L16-7.ASM

  ScanLoop:
@@ -52,7 +55,7 @@
 
   

John’s approach makes it clear that word-counting is nothing more than a fairly simple state machine. The interesting part, of course, is building the fastest state machine.

-

Level 3: Breakthrough

+

Level 3: Breakthrough

The boundaries between the levels of optimization are not sharply defined. In a sense, level 3 optimization is just like levels 1 and 2, but more so. At level 3, one takes whatever level 2 perspective seems most promising, and implements it as efficiently as possible on the x86. Even more than at level 2, at level 3 this means breaking out of familiar patterns of thinking.

@@ -113,22 +116,26 @@

Enough said, I trust.

-

Enough Word Counting Already!

+

Enough Word Counting Already!

Before I finish up this chapter, I’d like to mention that Terje Mathisen’s WC word-counting program, which I’ve mentioned previously and which is available, with source, on Bix, is in the ballpark with David’s code for performance. What’s more, Terje’s program handles 8-bit ASCII, counts lines as well as words, and supports user-definable separator sets. It’s wonderful code, well worth a look; it also happens to be a great word-counting utility. By the way, Terje builds his 64K table on the fly, at program initialization; this allows for customized tables, shrinks the size of the EXE, and, according to Terje’s calculations, takes less time than loading the table off disk as part of the EXE.

So, has David written the fastest possible word-counting code? Well, maybe—but I have a letter from Terry Holmes, of San Rafael, California, that calculates the theoretical maximum performance of native 386 word-counting code at 5.5 cycles/byte, which would be significantly faster than David’s code. Terry, alas, didn’t bother to implement his design, but maybe I’ll take a shot at it someday. It’d be fun, for sure—but jeez, I’ve got real work to do!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/17-01.html b/17-01.html index 2b8b55b..44f1f37 100644 --- a/17-01.html +++ b/17-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 17
The Game of Life

@@ -47,7 +50,7 @@

First, I’ll describe the ground rules of Life, implement a very straightforward version in C++, and then speed that version up by about eight times without using any drastically different approaches or any assembly. This may be a little tame for some of you, but be patient; for after that, we’ll haul out the big guns and move into the 30 to 40 times speed-up range. Then in the next chapter, I’ll show you how several programmers really floored it in taking me up on my second Optimization Challenge, which involved the Game of Life.

-

The Rules of the Game

+

The Rules of the Game

The Game of Life is ridiculously simple. There is a cellmap, consisting of a rectangular matrix of cells, each of which may initially be either on or off. Each cell has eight neighbors: two horizontally, two vertically, and four diagonally. For each succeeding generation of cells, the game logic determines whether each cell will be on or off according to the following rules:

@@ -61,16 +64,20 @@

All in all, Listing 17.1 is a clean, compact, and elegant implementation of the Game of Life. Were it not that the code is as slow as molasses, we could stop right here.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/17-02.html b/17-02.html index fa518f6..dfd8c2c 100644 --- a/17-02.html +++ b/17-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 17.1 L17-1.CPP

 /* C++ Game of Life implementation for any mode for which mode set
@@ -288,16 +291,20 @@ void show_text(int x, int y, char *text)
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/17-03.html b/17-03.html index f9cf8ec..eea6ba3 100644 --- a/17-03.html +++ b/17-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Where Does the Time Go?

How slow is Listing 17.1? Table 17.1 shows that even on a 486, Listing 17.1 does fewer than three 96x96 generations per second. (The times in Table 17.1 are for 1,000 generations of a 96x96 cell map with seed=1, LIMIT_18_HZ=0, WRAP_EDGES=1, and magnifier=2, running on a 33 MHz 486.) Since my target is 18 generations per second with a 200x200 cellmap on a 20 MHz 386, Listing 17.1 is too slow by a rather wide margin—75 times too slow, in fact. You might say we have a little optimizing to do.

@@ -182,16 +185,20 @@

Having said that, let me hasten to add that algorithmic improvements can make a big difference even when working at a purely abstract level. For a large unordered data set, a high-level Quicksort will beat the pants off the best-implemented insertion sort you can imagine. Still, you can optimize your algorithm from here ’til doomsday, and if you have a fast algorithm running on top of a highly abstract programming model, you’ll almost certainly end up with a slow program. In Listing 17.1, the abstraction that’s killing us is that of looking at the eight neighbors with eight completely independent operations, requiring eight calls to cell_state() and eight calculations of cell address and cell mask. In fact, given the nature of cell storage, the eight neighbors are in a fixed relationship to one another, and the addresses and masks of all eight can generally be found very easily via hard-wired offsets and shifts once the address and mask of any one is known.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/17-04.html b/17-04.html index 7e9e487..e22a81b 100644 --- a/17-04.html +++ b/17-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

There’s a kicker here, though, and that’s the counting of neighbors for cells at the edge of the cellmap. When cellmap wrapping is enabled (so that the cellmap becomes essentially a toroid, with each edge joined seamlessly to the opposite edge, as opposed to having a border of off-cells), neighbors that reside on the other edge of the cellmap can’t be accessed by the standard fixed offset, as shown in Figure 17.1. So, in general, we could improve performance by hard-wiring our neighbor-counting for the bit-per-cell cellmap format, but it seems we’d need a lot of conditional code to handle wrapping, and that would slow things back down again.


@@ -207,16 +210,20 @@ void cellmap::next_generation(cellmap& next_map) }

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/17-05.html b/17-05.html index 128a7a7..b5fac2b 100644 --- a/17-05.html +++ b/17-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

In Listing 17.3, note the padded cellmap edges, and the alteration of the member functions to compensate for the padding. Also note that the width now has to be a multiple of eight, to facilitate the process of copying the edges to the opposite padding bytes. We have decreased the generality of our Game of Life implementation in exchange for better performance. That’s a very common trade-off, as common as trading memory for performance. As a rule, the more general a program is, the slower it is. A corollary is that often (not always, but often), the more heavily optimized a program is, the more complex and the more difficult to implement it is. You can often improve performance a good deal by implementing only the level of generality you need, but at the same time decreased generality makes it more difficult to change or port the program at some later date. A Game of Life implementation, such as Listing 17.1, that’s built on set_cell(), clear_cell(), and get_cell() is completely general; you can change the cell storage format simply by changing the constructor and those three functions. Listing 17.3 is harder to change because count_neighbors() would also have to be altered, and it’s more complex than any of the other functions.

So, in Listing 17.3, we’ve gotten under the hood and changed the cellmap format a little, and gotten impressive results. But now count_neighbors() is hard-wired for optimized counting, and it’s still taking up more than half the time. Maybe now it’s time to go to assembly?

@@ -140,16 +143,20 @@ neighbor_count++; -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/17-06.html b/17-06.html index 5db9fd1..dd809f4 100644 --- a/17-06.html +++ b/17-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

We’re still not ready for assembly, though; what we need is a new perspective that lends itself to vastly better performance in C++. The Life program in the next section is three to seven times faster than Listing 17.4—and it’s still in C++.

How is this possible? Here are some hints:

@@ -50,7 +53,7 @@

I have two objectives to achieve in the remainder of this chapter. First, I want to show that optimization consists of many levels, from assembly language up to conceptual design, and that assembly language kicks in pretty late in the optimization process. Second, I want to encourage you to saturate your brain with everything you know about any particular optimization problem, then make space for your right brain to solve the problem.

-

Re-Examining the Task

+

Re-Examining the Task

Earlier in this chapter, we looked at a straightforward Game of Life implementation, then increased performance considerably by making the implementation a little less abstract and a little less general. We made a small change to the cellmap format, adding padding bytes off the edges so that pointer arithmetic would always work, but the major optimizations were moving the critical code into a single loop and using pointers rather than member functions whenever possible. In other words, we took what we already knew and made it more efficient.

@@ -75,22 +78,26 @@


Figure 17.3
  New cell format.

-

Acting on What We Know

+

Acting on What We Know

Once we’ve changed the cellmap format to store neighbor counts as well as states, with a byte for each cell, we can get another performance boost by again examining what we know about our data. I said earlier that most cells are off during any given generation. This means that most cells have no neighbors that are on. Since the cell map representation for an off-cell that has no neighbors is a zero byte, we can skip over scads of unchanged cells at a pop simply by scanning for non-zero bytes. This is much faster than explicitly testing cell states and neighbor counts, and lends itself beautifully to assembly language implementation as REPZ SCASB or (with a little cleverness) REPZ SCASW. (Unfortunately, there’s no C library function that can scan memory for the next byte that’s non-zero.)

Listing 17.5 is a Game of Life implementation that uses the neighbor-count cell map format and scans for non-zero bytes. On a 20 MHz 386, Listing 17.5 is about 4.5 times faster at calculating generations (that is, the generation engine is 4.5 times faster; I’m ignoring the time consumed by drawing and text display) than Listing 17.4, which is no slouch. On a 33 MHz 486, Listing 17.5 is about 3.5 times faster than Listing 17.4. This is true even though Listing 17.5 must be compiled using the large model. Imagine that—getting a four times speed-up while switching from the small model to the large model!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/17-07.html b/17-07.html index c59dd1f..11f9f0e 100644 --- a/17-07.html +++ b/17-07.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 17.5 L17-5.CPP

 /* C++ Game of Life implementation for any mode for which mode set
@@ -307,16 +310,20 @@ void cellmap::init()
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/17-08.html b/17-08.html index fe608b2..25913d2 100644 --- a/17-08.html +++ b/17-08.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The large model is actually not necessary for the 96x96 cellmap in Listing 17.5. However, I was actually more interested in seeing a fast 200x200 cellmap, and two 200x200 cellmaps can’t fit in a single segment. (This can easily be worked around in assembly language for cellmaps up to a segment in size; beyond that size, cellmap scanning becomes pretty complex, although it can still be efficiently implemented with some clever programming.)

Anyway, using the large model helps illustrate that it’s the data representation and the data processing approach you choose that matter most. Optimization details like memory models and segments and in-line functions and assembly language are important but secondary. Let your mind roam creatively before you start coding. Otherwise, you may find you’re writing well-tuned slow code, which is by no means the same thing as fast code.

@@ -48,7 +51,7 @@

No doubt we could get another two to five times improvement with good assembly code—but that’s dwarfed by a 30-times improvement, so optimization at a conceptual level must come first.

-

The Challenge That Ate My Life

+

The Challenge That Ate My Life

The most recent optimization challenge I laid my community of readers was to write the fastest possible Game of Life generation engine. By “engine” I meant that I didn’t care about time spent in input or output, only time consumed by the call to next-generation. The time spent updating the cellmap was what I wanted people to concentrate on.

@@ -72,16 +75,20 @@

Who won? What did I learn? To find out, read on.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/18-01.html b/18-01.html index d1a5594..f5b6b01 100644 --- a/18-01.html +++ b/18-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 18
It’s a plain Wonderful Life

@@ -63,16 +66,20 @@

Onward to the code.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/18-02.html b/18-02.html index 9524e99..086e543 100644 --- a/18-02.html +++ b/18-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Table-Driven Magic

David Stafford won my first Optimization Challenge by means of a huge look-up table and an incredible state machine driven by that table. The table didn’t cause David’s entry to exceed the line limit because David’s submission included code to generate the table on the fly as part of the build process. David has done himself one better this time with his QLIFE program; not only does his build process generate a 64K table, but it also generates virtually all his code, consisting of 17,000-plus lines of assembly language spanning another 64K. What David has done is write the equivalent of a bitblt compiler for the Game of Life; one might in fact call it a Life compiler. What David’s code generates is still a general-purpose program; it takes arbitrary seed values, and can run for an arbitrary number of generations, so it’s not as if David simply hardwired the instructions to draw each successive screen. However, it’s a general-purpose program that is exquisitely tailored to the task it needs to perform.

@@ -96,16 +99,20 @@
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/18-03.html b/18-03.html index 30d69ad..e6da839 100644 --- a/18-03.html +++ b/18-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 18.1 BUILD.BAT

 bcc -v -D%1=%2;%2=%3;%3=%4;%4=%5;%5=%6;%6=%7;%7=%8;%8 lcomp.c
@@ -535,16 +538,20 @@ void main( void )
   }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/18-04.html b/18-04.html index 7bdd8b2..abbc8f8 100644 --- a/18-04.html +++ b/18-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 18.3 MAIN.C

 // MAIN.C
@@ -198,16 +201,20 @@ extern unsigned short far ChangeList1[];
   


Figure 18.1
  Cell triplet storage.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/18-05.html b/18-05.html index 5357767..6ccd4d9 100644 --- a/18-05.html +++ b/18-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

“The basic idea is to maintain a ‘change list.’ This is an array of pointers into the cell array. Each change list element points to a word which changes in the next generation. This way we don’t have to waste time scanning every cell since most of them do not change. Two passes are made through the change list. The first pass updates the cell display on the screen, sets the life/death status of each cell for this new generation, and updates the neighbor counts for the adjacent cells. There are some efficiencies gained by using cell triplets rather than individual cells since we usually don’t need to set all eight neighbors. [Again, the neighbor counts for cells in the same word are implied by the states of those cells.] The second pass sets the next-generation states for the cells and their neighbors, and in the process builds the change list for the next generation.

“Processing each word is a little complex but very fast. A 64K block of code exists with routines on each 256-byte boundary. Generally speaking, the entry point corresponds to the high byte of the cell word. This byte contains the life/death values and a bit to indicate if this is an edge condition. During the first pass we take the cell triplet word, AND it with 0XFE00, and jump to that address. During the second pass we take the cell triplet word, AND it with 0xFE00, OR it with 0x0100, and jump to that address. [Therefore, there are 128 possible jump targets on the first pass, and 128 more on the second, all on 256-byte boundaries and all keyed off the high 7 bits of the cell triplet state; because bit 8 of the jump index is 0 on the first pass and 1 on the second, there is no conflict. The lower bit isn’t needed for other purposes because only the edge flag bit and the six life/death state bits matter for jumping into David’s state machine. The other nine bits, the bits used for the neighbor counts, are used only in the next step.]

@@ -48,7 +51,7 @@ FS : Video segment GS : Unused
-

A Layperson’s Overview of QLIFE

+

A Layperson’s Overview of QLIFE

Most likely, you’re scratching your head right now in bemusement. I don’t blame you; I felt the same way myself at first. It’s actually pretty simple, though, once you have the hang of it. Basically, David runs down the change list, visiting every cell that’s due to change in this generation, setting it to the new state, drawing it in the new state, and adjusting the counts of all its neighbors. David has a separate assembly routine for every possible change of state for a cell triplet, and he jumps to the proper routine by taking the cell triplet word, masking off the lower 9 bits, and jumping to the address where the appropriate code to perform that particular change of state resides. He does this for every entry in the change list. When this is completed, the current generation has been drawn and updated.

@@ -65,16 +68,20 @@ jmp dx

There Ain’t No Such Thing As the Fastest Code.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/19-01.html b/19-01.html index 2a31923..1e38cab 100644 --- a/19-01.html +++ b/19-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 19
Pentium: Not the Same Old Song

@@ -61,16 +64,20 @@

My first thought upon hearing of the Pentium’s dual pipes was to wonder how often the prefetch queue stalls for lack of instruction bytes, given that the demand for instruction bytes can be twice that of the 486. The answer is: rarely indeed, and then only because the code is not in the internal cache. The 486 has a single 8K cache that stores both code and data, and prefetching can stall if data fetching doesn’t allow time for prefetching to occur (although this rarely happens in practice).

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/19-02.html b/19-02.html index a6dbcd0..8468235 100644 --- a/19-02.html +++ b/19-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- @@ -40,7 +43,7 @@

(And yes, self-modifying code still works; as with all Pentium changes, the dual caches introduce no incompatibilities with 386/486 code.) Also, because the code and data caches are separate, code can’t be driven out of the cache in a tight loop that accesses a lot of data, unlike the 486. In addition, the Pentium expands the 486’s 32-byte prefetch queue to 128 bytes. In conjunction with the branch prediction feature (described next), which allows the Pentium to prefetch properly at most branches, this larger prefetch queue means that the Pentium’s two pipes should be better fed than those of any previous x86 processor.

-

Crossing Cache Lines

+

Crossing Cache Lines

There are three other characteristics of the Pentium that make for a healthy supply of instruction bytes. One is that the Pentium can prefetch instructions across cache lines. Unlike the 486, where there is a 3-cycle penalty for branching to an instruction that spans a cache line, there’s no such penalty on the Pentium. The second is that the cache line size (the number of bytes fetched from the external cache or main memory on a cache miss) on the Pentium is 32 bytes, twice the size of the 486’s cache line, so a cache miss causes a longer run of instructions to be placed in the cache than on the 486. The third is that the Pentium’s external bus is twice as wide as the 486’s, at 64 bits, and runs twice as fast, at 66 MHz, so the Pentium can fetch both instruction and data bytes from the external cache four times as fast as the 486.

@@ -56,7 +59,7 @@

One change in the Pentium that you definitely do have to worry about is superscalar execution. Utilization of the V-pipe can range from near zero percent to 100 percent, depending on the code being executed, and careful rearrangement of code can have amazing effects. Maxing out V-pipe use is not a trivial task; I’ll spend all of the next chapter discussing it so as to have time to cover it properly. In the meantime, two good references for superscalar programming and other Pentium information are Intel’s Pentium Processor User’s Manual: Volume 3: Architecture and Programming Manual (ISBN 1-55512-195-0; Intel order number 241430-001), and the article “Optimizing Pentium Code” by Mike Schmidt, in Dr. Dobb’s Journal for January 1994.

-

Cache Organization

+

Cache Organization

There are two other interesting changes in the Pentium’s cache organization. First, the cache is two-way set-associative, whereas the 486 is four-way set-associative. The details of this don’t matter, but simply put, this, combined with the 32-byte cache line size, means that the Pentium has somewhat coarser granularity in both space and time than the 486 in terms of packing bytes into the cache, although the total cache space is now bigger. There’s nothing you can do about this, but it may make it a little harder to get a loop’s working set into the cache. Second, the internal cache can now be configured (by the BIOS or OS; you won’t have to worry about it) for write-back rather than write-through operation. This means that writes to the internal data cache don’t necessarily get propagated to the external bus until other demands for cache space force the data out of the cache, making repeated writes to memory variables such as loop counters cheaper on average than on the 486, although not as cheap as registers.

@@ -82,16 +85,20 @@ add edx,4 ;V-pipe cycle 2

makes it functionally identical, but cuts the cycles to 2—a 50 percent improvement. Clearly, avoiding AGIs becomes a much more challenging and rewarding game in a superscalar world, one to which I’ll return in the next chapter.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/19-03.html b/19-03.html index 315e096..64280dd 100644 --- a/19-03.html +++ b/19-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Faster Addressing and More

I’ll spend the rest of this chapter covering a variety of Pentium optimization tips. For starters, effective address calculations (that is, the addition and scaling required to calculate a memory operand’s address, as for example in MOV EAX,[EBX+ECX*2+4]) never take any extra cycles on the Pentium (other than possibly an AGI cycle), even for the use of base+index addressing (as in MOV [ESI+EDI],EAX) or scaling (*2, *4, or *8, as in INC ARRAY[ESI*4]). On the 486, both of the latter cases cause a 1-cycle penalty. The faster effective address calculations have the side effect of making LEA very attractive as an arithmetic instruction. LEA can add any two registers, one of which can be multiplied by one, two, four, or eight, plus a constant value, and can store the result in any register—all in one cycle, apart from AGIs. Not only that, but as we’ll see in the next chapter, LEA can go through either pipe, whereas SHL can only go through the U-pipe, so LEA is often a superior choice for multiplication by three, four, five, eight, or nine. (ADD is the best choice for multiplication by two.) If you use LEA for arithmetic, do remember that unlike ADD and SHL, it doesn’t modify any flags.

@@ -65,16 +68,20 @@ mov eax,[ebx]

As with the 486, don’t use ENTER or LEAVE, which are slower than the equivalent discrete instructions. Also, start using TEST reg,reg instead of AND reg,reg or OR reg,reg to test whether a register is zero. The reason, as we’ll see in Chapter 21, is that TEST, unlike AND and OR, never modifies the target register. Although in this particular case AND and OR don’t modify the target register either, the Pentium has no way of knowing that ahead of time, so if AND or OR goes through the U-pipe, the Pentium may have to shut down the V-pipe for a cycle to avoid potential dependencies on the result of the AND or OR. TEST suffers from no such potential dependencies.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/19-04.html b/19-04.html index a47fce3..6bdfd80 100644 --- a/19-04.html +++ b/19-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Branch Prediction

One brand-spanking-new feature of the Pentium is branch prediction, whereby the Pentium tries to guess, based on past history, which way (or, for conditional jumps, whether or not), your code will jump at each branch, and prefetches along the likelier path. If the guess is correct, the branch or fall-through takes only 1 cycle—2 cycles less than a branch and the same as a fall-through on the 486; if the guess is wrong, the branch or fall-through takes 4 or 5 cycles (if it executes in the U- or V-pipe, respectively)—1 or 2 cycles more than a branch and 3 or 4 cycles more than a fall-through on the 486.

@@ -60,13 +63,13 @@

The Pentium has all the instructions of the 486, plus a few new ones. One much-needed instruction that has finally made it into the instruction set is CPUID, which allows your code to determine what processor it’s running on. CPUID is 15 years late, but at least it’s finally here. Another new instruction is CMPXCHG8B, which does a compare and conditional exchange on a qword. CMPXCHG8B doesn’t seem to me to be a particularly useful instruction, but I’m sure Intel wouldn’t have added it without a reason; if you know of a use for it, please pass it along to me.

-

486 versus Pentium Optimization

+

486 versus Pentium Optimization

Many Pentium optimizations help, or at least don’t hurt, on the 486. Many, but not all—and many do hurt on the 386. As I discuss various Pentium optimizations, I will attempt to note the effects on the 486 as well, but doing this in complete detail would double the sizes of these discussions and make them hard to follow. In general, I’d recommend reserving Pentium optimization for your most critical code, and even there, it’s a good idea to have at least two code paths, one for the 386 and one for the 486/Pentium. It’s also a good idea to time your code on a 486 before and after Pentium-optimizing it, to make sure you haven’t hurt performance on what will be, after all, by far the most important processor over the next couple of years.

With that in mind, is optimizing for the Pentium even worthwhile today? That depends on your application and its market—but if you want absolutely the best possible performance for your DOS and Windows apps on the fastest hardware, Pentium optimization can make your code scream.

-

Going Superscalar

+

Going Superscalar

In the next chapter, we’ll look into the single biggest element of Pentium performance, cranking up the Pentium’s second execution pipe. This is the area in which compiler technology is most touted for the Pentium, the two thoughts apparently being that (1) most existing code is in C, so recompiling to use the second pipe better is an automatic win, and (2) it’s so complicated to optimize Pentium code that only a compiler can do it well. The first point is a reasonable one, but it does suffer from one flaw for large programs, in that Pentium-optimized code is larger than 486- or 386-optimized code, for reasons that will become apparent in the next chapter. Larger code means more cache misses and more page faults; and while most of the code in any program is not critical to performance, compilers optimize code indiscriminately.

@@ -74,16 +77,20 @@

A compiler that generates better code than a good assembly programmer? That’ll be the day.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/20-01.html b/20-01.html index 62044f1..4e89502 100644 --- a/20-01.html +++ b/20-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 20
Pentium Rules

@@ -70,16 +73,20 @@

The key to Pentium optimization is to view execution as a stream of instructions going through the U- and V-pipes, and to eliminate, as much as possible, instruction mixes that take the V-pipe out of action. In practice, this is not too difficult. The only hard part is keeping in mind the long list of rules governing instruction pairing. The place to begin is with the set of instructions that can go through the V-pipe.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/20-02.html b/20-02.html index e77e7a3..ec8cb23 100644 --- a/20-02.html +++ b/20-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

V-Pipe-Capable Instructions

Any instruction can go through the U-pipe, and, for practical purposes, the U-pipe is always executing instructions. (The exceptions are when the U-pipe execution unit is waiting for instruction or data bytes after a cache miss, and when a U-pipe instruction finishes before a paired V-pipe instruction, as I’ll discuss below.) Only the instructions shown in Table 20.1 can go through the V-pipe. In addition, the V-pipe can execute a separate instruction only when one of the instructions listed in Table 20.2 is executing in the U-pipe; superscalar execution is not possible while any instruction not listed in Table 20.2 is executing in the U-pipe. So, for example, if you use SHR EDX,CL, which takes 4 cycles to execute, no other instructions can execute during those 4 cycles; if, on the other hand, you use SHR EDX,10, it will take 1 cycle to execute in the U-pipe, and another instruction can potentially execute concurrently in the V-pipe. (As you can see, similar instruction sequences can have vastly different performance characteristics on the Pentium.)

@@ -171,16 +174,20 @@ mov [MemVar],edx -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/20-03.html b/20-03.html index 7ff97de..02c69a6 100644 --- a/20-03.html +++ b/20-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Lockstep Execution

You may wonder why anyone would bother breaking ADD [MemVar],EAX into three instructions, given that this instruction can go through either pipe with equal ease. The answer is that while the memory-accessing instructions other than MOV, PUSH, and POP listed in Table 20.1 (that is, INC/DEC [mem], ADD/SUB/XOR/AND/OR/CMP/ADC/SBB reg,[mem], and ADD/SUB/XOR/AND/OR/CMP/ADC/SBB [mem],reg/immed) can be paired, they do not provide the 100 percent overlap that we seek. If you look at Tables 20.1 and 20.2, you will see that instructions taking from 1 to 3 cycles can pair. However, any pair of instructions goes through the two pipes in lockstep. This means, for example, that if ADD [EBX],EDX is going through the U-pipe, and INC EAX is going through the V-pipe, the V-pipe will be idle for 2 of the 3 cycles that the U-pipe takes to execute its instruction, as shown in Figure 20.4. Out of the theoretical 6 cycles of work that can be done during this time, we actually get only 4 cycles of work, or 67 percent utilization. Even though these instructions pair, then, this sequence fails to make maximum use of the Pentium’s horsepower.

@@ -82,16 +85,20 @@ add esi,[SourceSkip] ;U-pipe cycles 1 and 2 add edi,[DestinationSkip] ;V-pipe cycles 1 and 2
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/20-04.html b/20-04.html index ddeb1d8..4adfd5a 100644 --- a/20-04.html +++ b/20-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

However, this beneficial pairing does not extend to non-MOV instructions with explicit memory destination operands, such as ADD [EBX],EAX. The Pentium executes only one such memory instruction at a time; if two memory-destination complex instructions get paired, first the U-pipe instruction is executed, and then the V-pipe instruction, with only one cycle of overlap, as shown in Figure 20.6. I don’t know for sure, but I’d guess that this is to guarantee that the two pipes will never perform out-of-order access to any given memory location. Thus, even though AND [EBX],AL pairs with AND [ECX],DL, the two instructions take 5 cycles in all to execute, and 4 cycles of idle time—2 in the U-pipe and 2 in the V-pipe, out of 10 cycles in all—are incurred in the process.


@@ -58,7 +61,7 @@ mov [ebx],dl

What we’ve just seen is the read-after-write form of the superscalar hazard known as register contention. I’ll return to the subject of register contention in the next chapter; in the remainder of this chapter I’d like to cover a few short items about superscalar execution.

-

Register Starvation

+

Register Starvation

The above examples should make it pretty clear that effective superscalar programming puts a lot of strain on the Pentium’s relatively small register set. There are only seven general-purpose registers (I strongly suggest using EBP in critical loops), and it does not help to have to sacrifice one of those registers for temporary storage on each complex memory operation; in pre-superscalar days, we used to employ those handy CISC memory instructions to do all that stuff without using any extra registers.

@@ -79,16 +82,20 @@ mov [ebx],dl


Figure 20.8
  Prefix delays.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/21-01.html b/21-01.html index 25215c9..9b2f31f 100644 --- a/21-01.html +++ b/21-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 21
Unleashing the Pentium’s V-Pipe

@@ -74,16 +77,20 @@ mov edx,[ebp-8] ;V-pipe cycle 3 lockstep idle


Figure 21.2
  An AGI can cost as many as 3 cycles.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/21-02.html b/21-02.html index b92c840..b9a05d2 100644 --- a/21-02.html +++ b/21-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

As on the 486, you should keep a careful eye out for AGIs involving the stack pointer. Implicit modifiers of ESP, such as PUSH and POP, are special-cased so you don’t have to worry about AGIs. However, if you explicitly modify ESP with this instruction

 sub esp,100h
@@ -92,7 +95,7 @@ mov al,[Var]  ;U-pipe cycle 2
 
   

where an attempt is made to set both EAX and its AL subregister on the same cycle. Write-after-write contention implies that the two instructions comprising the above substitute for MOVZX should have at least one unrelated instruction between them when SUB EAX,EAX executes in the V-pipe.

-

Exceptions to Register Contention

+

Exceptions to Register Contention

Intel has special-cased some very useful exceptions to register contention. Happily, write-after-read operations do not cause contention. Such operations, as in

@@ -140,16 +143,20 @@ LoopTop:
                      ; pair in the U-pipe
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/21-03.html b/21-03.html index 473c8a3..eb56a55 100644 --- a/21-03.html +++ b/21-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

It’s actually not hard to figure out which instructions go through which pipes; just back up until you find an instruction that can’t pair or can only go through the U-pipe, and work forward from there, given the knowledge that that instruction executes in the U-pipe. The easiest thing to look for is branches. All branch target instructions execute in the U-pipe, as do all instructions after conditional branches that fall through. Instructions with prefix bytes are generally good U-pipe markers, although they’re expensive instructions that should be avoided whenever possible, and have at least one aberration with regard to pipe usage, as discussed below. Shifts, rotates, ADC, SBB, and all other instructions not listed in Table 20.1 in the last chapter are likewise U-pipe markers.

Pentium Optimization in Action

@@ -72,16 +75,20 @@ ckloop:

The first thing to do is to time the code in Listing 21.1 to verify our analysis. When I unleashed the Zen timer on Listing 21.1, I found, to my surprise, that the code actually takes only five cycles per checksum word processed, not six. A little more experimentation revealed that adding a size prefix to the two-cycle ADD EAX,[ESI] instruction doesn’t cost anything, certainly not the one full cycle in each pipe that a prefix is supposed to take. More experimentation showed that prefix bytes do cost the documented extra cycle when used with one-cycle instructions such as MOV. At this point, my preliminary conclusion is that prefixes can pair with the first cycle of at least some multiple-cycle instructions. Determining exactly why this happens will take further research on my part, but the most important conclusion is that you must measure your code!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/21-04.html b/21-04.html index 27e36c3..ddd53cb 100644 --- a/21-04.html +++ b/21-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The first, obvious thing we can do to Listing 21.1 is change ADC AX,0 to ADC EAX,0, eliminating a prefix byte and saving a full cycle. Now we’re down from five to four cycles. What next?

Listing 21.2 shows one interesting alternative that doesn’t really buy us anything. Here, we’ve eliminated all size prefixes by doing byte-sized MOVs and ADDs, but because the size prefix on ADD AX,[ESI], for whatever reason, didn’t cost anything in Listing 21.1, our efforts are to no avail—Listing 21.2 still takes 4 cycles per checksummed word. What’s worth noting about Listing 21.2 is the extent to which the code is broken into simple instructions and reordered so as to avoid size prefixes, register contention, AGIs, and data bank conflicts (the latter because both [ESI] and [ESI+1] are in the same cache data bank, as discussed in the last chapter).

@@ -120,16 +123,20 @@ ckloopdone:

Listing 21.3 could be made a bit faster yet with some loop unrolling, but that would make the code quite a bit more complex for relatively little return. Instead, why not make the code more complex and get a big return? Listing 21.4 does exactly that by loading one dword at a time to eliminate both the word prefix of Listing 21.1 and the multiple byte-sized accesses of Listing 21.3. An obvious drawback to this is the considerable complexity needed to ensure that the dword accesses are dword-aligned (remember that unaligned dword accesses cost three cycles each), and to handle buffer lengths that aren’t dword multiples. I’ve handled these problems by requiring that the buffer be dword-aligned and a dword multiple in length, which is of course not always the case in the real world. However, the point of these listings is to illustrate Pentium optimization—dword issues, being non-inner-loop stuff, are solvable details that aren’t germane to the main focus. In any case, the complexity and assumptions are well justified by the performance of this code: three cycles per loop, or 1.5 cycles per checksummed word, more than three times the speed of the original code. Again, note that the actual order in which the instructions are arranged is dictated by the various optimization hazards of the Pentium.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/21-05.html b/21-05.html index a1c6b89..bf04145 100644 --- a/21-05.html +++ b/21-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 21.4 L21-4.ASM

 ; Calculates TCP/IP (16-bit carry-wrapping) checksum for buffer
@@ -114,20 +117,24 @@ ckloopdone:
 
   

I don’t claim that Listing 21.5 is the fastest possible way to do a TCP/IP checksum on a Pentium; in fact, it isn’t. Unrolling the loop one more time, together with a trick of Terje’s that uses LEA to advance ESI (neither LEA nor DEC affects the carry flag, allowing Terje to add the carry from the previous loop iteration into the next iteration’s checksum via ADC), produces a version that’s a full 33 percent faster. Nonetheless, Listings 21.1 through 21.5 illustrate many of the techniques and considerations in Pentium optimization. Hand-optimization for the Pentium isn’t simple, and requires careful measurement to check the efficacy of your optimizations, so reserve it for when you really, really need it—but when you need it, you need it bad.

-

A Quick Note on the 386 and 486

+

A Quick Note on the 386 and 486

I’ve mentioned that Pentium-optimized code does fine on the 486, but not always so well on the 386. On a 486, Listing 21.1 runs at 9 cycles per checksummed word, and Listing 21.5 runs at 2.5 cycles per checksummed word, a healthy 3.6-times speedup. On a 386, Listing 21.1 runs at 22 cycles per word; Listing 21.5 runs at 7 cycles per word, a 3.1-times speedup. As is often the case, Pentium optimization helped the other processors, but not as much as it helped the Pentium, and less on the 386 than on the 486.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/22-01.html b/22-01.html index aa5e739..6b66498 100644 --- a/22-01.html +++ b/22-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 22
Zenning and the Flexible Mind

@@ -91,16 +94,20 @@ ClearS endp

Well, that was certainly easy enough! We’re not going to find any more totally non-functional instructions in ClearS, however, so let’s get on to some serious optimizing. We’ll look first for cases where we know of better instructions for particular tasks than those that were chosen. For example, there’s no need to load any register, whether segment or general, through BX; we can eliminate two instructions by loading ES and DI directly as shown in Listing 22.2.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/22-02.html b/22-02.html index 943b525..b42ca00 100644 --- a/22-02.html +++ b/22-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 22.2 L22-2.ASM

 ClearS        proc near
@@ -121,16 +124,20 @@ ClearS         endp
 
   

Let’s step back and see what this code really does, though. All it does in the end is load one byte addressed relative to BP into AH and another byte addressed relative to BP into AL. Heck, we can just do that directly! Presto—we’ve saved another 6 bytes, and turned two word-sized memory accesses into byte-sized memory accesses as well. Listing 22.5 shows the new code.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/22-03.html b/22-03.html index 635cbfa..0d36727 100644 --- a/22-03.html +++ b/22-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 22.5 L22-5.ASM

 ClearS         proc near
@@ -113,16 +116,20 @@ ClearS         endp
 
   

All in all, the Zenned version of ClearS is a vast improvement over the original. Probably not the best possible implementation—never say never!—but an awfully good one.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/23-01.html b/23-01.html index e317652..b34792b 100644 --- a/23-01.html +++ b/23-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Part II

Chapter 23
@@ -63,16 +66,20 @@

Listing 23.1 is a sample VGA program that pans around an animated 16-color medium-resolution (640x350) playfield. There’s a lot packed into this code; I’m going to focus on the VGA-specific aspects so we don’t get sidetracked. I’m not going to explain how the ball is animated, for example; we’ll get to animation starting in Chapter 42. What I will do is cover each of the VGA features used in this program—the virtual screen, vertical and horizontal panning, color plane manipulation, multi-plane block copying, and page flipping—at a conceptual level, letting the code itself demonstrate the implementation details. We’ll return to many of these concepts in more depth later in this book.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/23-02.html b/23-02.html index 659fe2b..4b072cc 100644 --- a/23-02.html +++ b/23-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

At the Core

A little background is necessary before we’re ready to examine Listing 23.1. The VGA is built around four functional blocks, named the CRT Controller (CRTC), the Sequence Controller (SC), the Attribute Controller (AC), and the Graphics Controller (GC). The single-chip VGA could have been designed to treat the registers for all the blocks as one large set, addressed at one pair of I/O ports, but in the EGA, each of these blocks was a separate chip, and the legacy of EGA compatibility is why each of these blocks has a separate set of registers and is addressed at different I/O ports in the VGA.

@@ -218,16 +221,20 @@ -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/23-03.html b/23-03.html index d6f8e3b..8912899 100644 --- a/23-03.html +++ b/23-03.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Linear Planes and True VGA Modes

+

Linear Planes and True VGA Modes

The VGA’s memory is organized as four 64K planes. Each of these planes is a linear bitmap; that is, each byte from a given plane controls eight adjacent pixels on the screen, the next byte controls the next eight pixels, and so on to the end of the scan line. The next byte then controls the first eight pixels of the next scan line, and so on to the end of the screen.

@@ -606,16 +609,20 @@ cseg ends end start
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/23-04.html b/23-04.html index dc96bf1..3cb9c4f 100644 --- a/23-04.html +++ b/23-04.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Smooth Panning

+

Smooth Panning

The first thing you’ll notice upon running the sample program is the remarkable smoothness with which the display pans from side-to-side and up-and-down. That the display can pan at all is made possible by two VGA features: 256K of display memory and the virtual screen capability. Even the most memory-hungry of the VGA modes, mode 12H (640x480), uses only 37.5K per plane, for a total of 150K out of the total 256K of VGA memory. The medium-resolution mode, mode 10H (640x350), requires only 28K per plane, for a total of 112K. Consequently, there is room in VGA memory to store more than two full screens of video data in mode 10H (which the sample program uses), and there is room in all modes to store a larger virtual screen than is actually displayed. In the sample program, memory is organized as two virtual screens, each with a resolution of 672x384, as shown in Figure 23.2. The area of the virtual screen actually displayed at any given time is selected by setting the display memory address at which to begin fetching video data; this is set by way of the start address registers (Start Address High, CRTC register 0CH, and Start Address Low, CRTC register 0DH). Together these registers make up a 16-bit display memory address at which the CRTC begins fetching data at the beginning of each video frame. Increasing the start address causes higher-memory areas of the virtual screen to be displayed. For example, the Start Address High register could be set to 80H and the Start Address Low register could be set to 00H in order to cause the display screen to reflect memory starting at offset 8000H in each plane, rather than at the default offset of 0.

@@ -57,16 +60,20 @@ -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/23-05.html b/23-05.html index d9df83d..7aa51f7 100644 --- a/23-05.html +++ b/23-05.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Color Plane Manipulation

+

Color Plane Manipulation

The VGA provides a considerable amount of hardware assistance for manipulating the four display memory planes. Two features illustrated by the sample program are the ability to control which planes are written to by a CPU write and the ability to copy four bytes—one from each plane—with a single CPU read and a single CPU write.

@@ -50,7 +53,7 @@

Don’t worry if you’re not catching everything in this chapter on the first pass; the VGA is a complicated beast, and learning about it is an iterative process. We’ll be going over these features again, in different contexts, over the course of the rest of this book.

-

Page Flipping

+

Page Flipping

When animated graphics are drawn directly on the screen, with no intermediate frame-composition stage, the image typically flickers and/or ripples, an unavoidable result of modifying display memory at the same time that it is being scanned for video data. The display memory of the VGA makes it possible to perform page flipping, which eliminates such problems. The basic premise of page flipping is that one area of display memory is displayed while another is being modified. The modifications never affect an area of memory as it is providing video data, so no undesirable side effects occur. Once the modification is complete, the modified buffer is selected for display, causing the screen to change to the new image in a single frame’s time, typically 1/60th or 1/70th of a second. The other buffer is then available for modification.

@@ -62,16 +65,20 @@

Clearly, what we want is to set the new start address, then wait for the start of the vertical sync pulse, at which point we can be sure the page has flipped. However, we can’t just set the start address and wait, because we might have the extreme misfortune to set one of the start address registers before the start of vertical sync and the other after, resulting in mismatched halves of the start address and a nasty jump of the displayed image for one frame.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/23-06.html b/23-06.html index ff8c7d7..32f5c7f 100644 --- a/23-06.html +++ b/23-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

One possible solution to this problem is to pick a second page start address that has a 0 value for the lower byte, so only the Start Address High register ever needs to be set, but in the sample program in Listing 23.1 I’ve gone for generality and always set both bytes. To avoid mismatched start address bytes, the sample program waits for pixel data to be displayed, as indicated by the Display Enable status; this tells us we’re somewhere in the displayed portion of the frame, far enough away from vertical sync so we can be sure the new start address will get used at the next vertical sync. Once the Display Enable status is observed, the program sets the new start address, waits for vertical sync to happen, sets the new pel panning state, and then continues drawing. Don’t worry about the details right now; page flipping will come up again, at considerably greater length, in later chapters.

@@ -64,16 +67,20 @@

Now that we know what the VGA looks like in broad strokes and have a sense of what VGA programming is like, we can start looking at specific areas in depth. In the next chapter, we’ll take a look at the hardware assistance the VGA provides the CPU during display memory access. There are four latches and four ALUs in those chips, along with some useful masks and comparators, and it’s that hardware that’s the difference between sluggish performance and making the VGA get up and dance.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/24-01.html b/24-01.html index 3fb6bb3..74e3d4f 100644 --- a/24-01.html +++ b/24-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 24
Parallel Processing with the VGA

@@ -58,16 +61,20 @@

All graphics in the sample program are done in black-and-white by writing to all planes, in order to show the operation of the ALUs most clearly. Selective enabling of planes via the Map Mask register and/or set/reset would produce color effects; in that case, the operation of the logical functions must be evaluated on a plane-by-plane basis, since only the enabled planes would be affected by each operation.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/24-02.html b/24-02.html index fe593de..0734bce 100644 --- a/24-02.html +++ b/24-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 24.1 L24-1.ASM

 ; Program to illustrate operation of ALUs and latches of the VGA’s
@@ -286,16 +289,20 @@ cseg    ends
 
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/24-03.html b/24-03.html index 2ce14ca..36f1e76 100644 --- a/24-03.html +++ b/24-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Logical function 0, which writes the CPU data unmodified, is the standard mode of operation of the ALUs. In this mode, the CPU data is combined with the latched data by ignoring the latched data entirely. Expressed as a logical function, this could be considered CPU data ANDed with 1 (or ORed with 0). This is the mode to use whenever you want to place CPU data into display memory, replacing the previous contents entirely. It may occur to you that there is no need to latch display memory at all when the data unmodified function is selected. In the sample program, that is true, but if the bit mask is being used, the latches must be loaded even for the data unmodified function, as I’ll discuss in the next chapter.

Logical functions 1 through 3 cause the CPU data to be ANDed, ORed, and XORed with the latched data, respectively. Of these, XOR is the most useful, since exclusive-ORing is a traditional way to perform animation. The uses of the AND and OR logical functions are less obvious. AND can be used to mask a blank area into display memory, or to mask off those portions of a drawing operation that don’t overlap an existing display memory image. OR could conceivably be used to force an image into display memory over an existing image. To be honest, I haven’t encountered any particularly valuable applications for AND and OR, but they’re the sort of building-block features that could come in handy in just the right context, so keep them in mind.

@@ -69,16 +72,20 @@ MOV DX,(VALUE2 SHL 8) OR VALUE1

The latches and ALUs are central to high-performance VGA code, since they allow programs to process across all four memory planes without a series of OUTs and read/write operations. It is not always easy to arrange a program to exploit this power, however, because the ALUs are far more limited than a CPU. In many instances, however, additional hardware in the VGA, including the bit mask, the set/reset features, and the barrel shifter, can assist the ALUs in controlling data, as we’ll see in the next few chapters.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/25-01.html b/25-01.html index 850afb7..398c410 100644 --- a/25-01.html +++ b/25-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 25
VGA Data Machinery

@@ -61,16 +64,20 @@

Listing 25.1 shows a program that uses the bit mask data rotation capabilities of the GC to draw bitmapped text at any screen location. The BIOS only draws characters on character boundaries; in 640x480 graphics mode the default font is drawn on byte boundaries horizontally and every 16 scan lines vertically. However, with direct bitmapped text drawing of the sort used in Listing 25.1, it’s possible to draw any font of any size anywhere on the screen (and a lot faster than via DOS or the BIOS, as well).

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/25-02.html b/25-02.html index b361f29..51be40f 100644 --- a/25-02.html +++ b/25-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 25.1 L25-1.ASM

 ; Program to illustrate operation of data rotate and bit mask
@@ -268,16 +271,20 @@ cseg    ends
 
   

Basically, the bit mask is handy whenever only some of the eight pixels in a byte of display memory need to be changed, because it allows full use of the VGA’s four-way parallel processing capabilities for the pixels that are to be drawn, without interfering with the pixels that are to be left unchanged. The alternative would be plane-by-plane processing, which from a performance perspective would be undesirable indeed.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/25-03.html b/25-03.html index 6025900..3fa5c56 100644 --- a/25-03.html +++ b/25-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

It’s worth pointing out again that the bit mask operates on the data in the latches, not on the data in display memory. This makes the bit mask a flexible resource that with a little imagination can be used for some interesting purposes. For example, you could fill the latches with a solid background color (by writing the color somewhere in display memory, then reading that location to load the latches), and then use the Bit Mask register (or write mode 3, as we’ll see later) as a mask through which to draw a foreground color stencilled into the background color without reading display memory first. This only works for writing whole bytes at a time (clipped bytes require the use of the bit mask; unfortunately, we’re already using it for stencilling in this case), but it completely eliminates reading display memory and does foreground-plus-background drawing in one blurry-fast pass.

@@ -59,16 +62,20 @@

The program in Listing 25.2 illustrates this problem. A green pattern (plane 1 set to 1, planes 0, 2, and 3 set to 0) is first written to display memory. Display memory is then filled with blue (only plane 0 set to 1), with a Map Mask setting of 01H. Where the blue crosses the green, cyan is produced, rather than blue, because the Map Mask register setting of 01H that produces blue leaves the green plane (plane 1) unchanged. In order to generate blue unconditionally, it would be necessary to set the Map Mask register to 0FH, clear memory, and then set the Map Mask register to 01H and fill with blue.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/25-04.html b/25-04.html index 95d619b..b453c83 100644 --- a/25-04.html +++ b/25-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 25.2 L25-2.ASM

 ; Program to illustrate operation of Map Mask register when drawing
@@ -115,7 +118,7 @@ cseg    ends
         end     start
 
-

Setting All Planes to a Single Color

+

Setting All Planes to a Single Color

The set/reset circuitry can be used to force some planes to 0-bits and others to 1-bits during a single write, while letting CPU data go to still other planes, and so provides an efficient way to set all planes to a desired color. The set/reset circuitry works as follows:

@@ -125,16 +128,20 @@ cseg ends

Listing 25.3 illustrates the use of set/reset to force a specific color to be written. This program is the same as that of Listing 25.2, except that set/reset rather than the Map Mask register is used to control color. The preexisting pattern is completely overwritten this time, because the set/reset circuitry writes 0-bytes to planes that must be off as well as 0FFH-bytes to planes that must be on.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/25-05.html b/25-05.html index 08a757f..19d7082 100644 --- a/25-05.html +++ b/25-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 25.3 L25-3.ASM

 ; Program to illustrate operation of set/reset circuitry to force
@@ -142,7 +145,7 @@ cseg    ends
         end     start
 
-

Manipulating Planes Individually

+

Manipulating Planes Individually

Listing 25.4 illustrates the use of set/reset to control only some, rather than all, planes. Here, the set/reset circuitry forces plane 2 to 1 and planes 0 and 3 to 0. Because bit 1 of the Enable Set/Reset register is 0, however, set/reset does not affect plane 1; the CPU data goes unchanged to the plane 1 ALU. Consequently, the CPU data can be used to control the value written to plane 1. Given the settings of the other three planes, this means that each bit of CPU data that is 1 generates a brown pixel, and each bit that is 0 generates a red pixel. Writing alternating bytes of 07H and 0E0H, then, creates a vertically striped pattern of brown and red.

@@ -260,16 +263,20 @@ cseg ends end start
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/25-06.html b/25-06.html index 6804722..5919022 100644 --- a/25-06.html +++ b/25-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

There is no clearly defined role for the set/reset circuitry, as there is for, say, the bit mask. In many cases, set/reset is largely interchangeable with CPU data, particularly with CPU data written in write mode 2 (write mode 2 operates similarly to the set/reset circuitry, as we’ll see in Chapter 27). The most powerful use of set/reset, in my experience, is in applications such as the example of Listing 25.4, where it is used to force the value written to certain planes while the CPU data is written to other planes. In general, though, think of set/reset as one more tool you have at your disposal in getting the VGA to do what you need done, in this case a tool that lets you force all bits in each plane to either zero or one, or pass CPU data through unchanged, on each write to display memory. As tools go, set/reset is a handy one, and it’ll pop up often in this book.

Notes on Set/Reset

@@ -48,16 +51,20 @@

In the early days of the EGA and VGA, there was considerable debate about whether it was safe to do word OUTs (OUT DX,AX) to set Index/Data register pairs in a single instruction. Long ago, there were a few computers with buses that weren’t quite PC-compatatible, in that the two bytes in each word OUT went to the VGA in the wrong order: Data register first, then Index register, with predictably disastrous results. Consequently, I generally wrote my code in those days to use two 8-bit OUTs to set indexed registers. Later on, I made it a habit to use macros that could do either one 16-bit OUT or two 8-bit OUTs, depending on how I chose to assemble the code, and in fact you’ll find both ways of dealing with OUTs sprinkled through the code in this part of the book. Using macros for word OUTs is still not a bad idea in that it does no harm, but in my opinion it’s no longer necessary. Word OUTs are standard now, and it’s been a long time since I’ve heard of them causing any problems.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/26-01.html b/26-01.html index 97550a4..6404a78 100644 --- a/26-01.html +++ b/26-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 26
VGA Write Mode 3

@@ -49,16 +52,20 @@

Listing 26.1 is a modification of code I presented in Chapter 25. That code used the data rotate and bit mask features of the VGA to draw bit-mapped text in write mode 0. Listing 26.1 uses write mode 3 in place of the bit mask to draw bit-mapped text, and in the process gains the useful ability to preserve the background into which the text is being drawn. Where the original text-drawing code drew the entire character box for each character, with 0 bits in the font pattern causing a black box to appear around each character, the code in Listing 26.1 affects display memory only when 1 bits in the font pattern are drawn. As a result, the characters appear to be painted into the background, rather than over it. Another advantage of the code in Listing 26.1 is that the characters can be drawn in any of the 16 available colors.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/26-02.html b/26-02.html index f1b767c..7811962 100644 --- a/26-02.html +++ b/26-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 26.1 L26-1.ASM

 ; Program to illustrate operation of write mode 3 of the VGA.
@@ -327,16 +330,20 @@ cseg    ends
         end     start
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/26-03.html b/26-03.html index 31500bf..fdd5518 100644 --- a/26-03.html +++ b/26-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The key to understanding Listing 26.1 is understanding the effect of ANDing the rotated CPU data with the contents of the Bit Mask register. The CPU data is the pattern for the character to be drawn, with bits equal to 1 indicating where character pixels are to appear. The Data Rotate register is set to rotate the CPU data to pixel-align it, since without rotation characters could only be drawn on byte boundaries.

@@ -380,16 +383,20 @@ cseg ends

If you take a quick look, you’ll see that the code in Listing 26.1 uses the readable register feature of the VGA to preserve reserved bits and bits other than those being modified. Older adapters such as the CGA and EGA had few readable registers, so it was necessary to set all bits in a register whenever that register was modified. Happily, all VGA registers are readable, which makes it possible to change only those bits of immediate interest, and, in general, I highly recommend doing exactly that, since IBM (or clone manufacturers) may well someday use some of those reserved bits or change the meanings of some of the bits that are currently in use.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/27-01.html b/27-01.html index e393801..ec5f7e0 100644 --- a/27-01.html +++ b/27-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 27
Yet Another VGA Write Mode

@@ -51,7 +54,7 @@

It’s possible that you understand write mode 2 thoroughly at this point; nonetheless, I suspect that some additional explanation of an admittedly non-obvious mode wouldn’t hurt. Let’s follow the CPU byte through the VGA in write mode 2, step by step.

-

A Byte’s Progress in Write Mode 2

+

A Byte’s Progress in Write Mode 2

Figure 27.1 shows the write mode 2 data path. The CPU byte comes into the VGA and is split into four separate bits, one for each plane. Bits 7-4 of the CPU byte vanish into the bit bucket, never to be heard from again. Speculation long held that those 4 unused bits indicated that IBM would someday come out with an 8-plane adapter that supported 256 colors. When IBM did finally come out with a 256-color mode (mode 13H of the VGA), it turned out not to be planar at all, and the upper nibble of the CPU byte remains unused in write mode 2 to this day.

@@ -74,16 +77,20 @@

Write mode 2 is selected by setting bits 1 and 0 of the Graphics Mode register (Graphics Controller register 5) to 1 and 0, respectively. Since VGA registers are readable, the correct way to select write mode 2 on the VGA is to read the Graphics Mode register, mask off bits 1 and 0, OR in 00000010b (02H), and write the result back to the Graphics Mode register, thereby leaving the other bits in the register undisturbed.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/27-02.html b/27-02.html index 02f8eab..acc23a8 100644 --- a/27-02.html +++ b/27-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Copying Chunky Bitmaps to VGA Memory Using Write Mode 2

+

Copying Chunky Bitmaps to VGA Memory Using Write Mode 2

Let’s take a look at two examples of write mode 2 in action. Listing 27.1 presents a program that uses write mode 2 to copy a graphics image in chunky format to the VGA. In chunky format adjacent bits in a single byte make up each pixel: mode 4 of the CGA, EGA, and VGA is a 2-bit-per-pixel chunky mode, and mode 13H of the VGA is an 8-bit-per-pixel chunky mode. Chunky format is convenient, since all the information about each pixel is contained in a single byte; consequently chunky format is often used to store bitmaps in system memory.

@@ -261,16 +264,20 @@ Code ends end Start
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/27-03.html b/27-03.html index df16049..8e1bca6 100644 --- a/27-03.html +++ b/27-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

“That’s an interesting application of write mode 2,” you may well say, “but is it really useful?” While the ability to convert chunky bitmaps into VGA bitmaps does have its uses, Listing 27.1 is primarily intended to illustrate the mechanics of write mode 2.

@@ -40,7 +43,7 @@
-

Drawing Color-Patterned Lines Using Write Mode 2

+

Drawing Color-Patterned Lines Using Write Mode 2

A more serviceable use of write mode 2 is shown in the program presented in Listing 27.2. The program draws multicolored horizontal, vertical, and diagonal lines, basing the color patterns on passed color tables. Write mode 2 is ideal because in this application color can vary from one pixel to the next, and in write mode 2 all that’s required to set pixel color is a change of the lower nibble of the byte written by the CPU. Set/reset could be used to achieve the same result, but an index/data pair of OUTs would be required to set the Set/Reset register to each new color. Similarly, the Map Mask register could be used in write mode 0 to set pixel color, but in this case not only would an index/data pair of OUTs be required but there would also be no guarantee that data already in display memory wouldn’t interfere with the color of the pixel being drawn, since the Map Mask register allows only selected planes to be drawn to.

@@ -376,16 +379,20 @@ Code ends end Start
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/27-04.html b/27-04.html index 14175a5..24e525f 100644 --- a/27-04.html +++ b/27-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

When to Use Write Mode 2 and When to Use Set/Reset

As indicated earlier, write mode 2 and set/reset are functionally interchangeable. Write mode 2 lends itself to more efficient implementations when the drawing color changes frequently, as in Listing 27.2.

@@ -62,16 +65,20 @@

At any rate, once the graphics mode bitmap is relocated, flipping to text mode and back becomes painless. The memory used by mode 3 doesn’t overlap the relocated mode 10H bitmap at all (unless additional portions of font memory are loaded), so all you need do is set bit 7 of AL on mode sets in order to flip back and forth between the two modes.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/27-05.html b/27-05.html index 6fb2428..afd864b 100644 --- a/27-05.html +++ b/27-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Another interesting point about flipping from graphics to text and back is that the standard mode 3 character/attribute map doesn’t actually take up every byte of the first 4000 bytes of planes 0 and 1. The standard mode 3 character/attribute map actually only takes up every even byte of the first 4000 in each plane; the odd bytes are left untouched. This means that only about 12K bytes actually have to be saved when going to text mode. The code in Listing 27.3 flips from graphics mode to text mode and back, saving only those 12K bytes that actually have to be saved. This code saves and restores the first 8K of plane 2 (the font area) while in graphics mode, but performs the save and restore of the 4000 bytes used for the character/attribute map while in text mode, because the characters and attributes, which are actually stored in the even bytes of planes 0 and 1, respectively, appear to be contiguous bytes in memory in text mode and so are easily saved as a single block.

Explaining why only every other byte of planes 0 and 1 is used in text mode and why characters and attributes appear to be contiguous bytes when they are actually in different planes is a large part of the explanation I’m not going to go into now. One bit of fallout from this, however, is that if you flip to text mode and preserve the graphics bitmap using the mechanism illustrated in Listing 27.3, you shouldn’t write to any text page other than page 0 (that is, don’t write to any offset in display memory above 3999 in text mode) or alter the Page Select bit in the Miscellaneous Output register (3C2H) while in text mode. In order to allow completely unfettered access to text pages, it would be necessary to save every byte in the first 32K of each of planes 0 and 1. (On the other hand, this would allow up to 16 text screens to be stored simultaneously, with any one displayable instantly.) Moreover, if any fonts other than the default font are loaded, the portions of plane 2 that those particular fonts are loaded into would have to be saved, up to a maximum of all 64K of plane 2. In the worst case, a full 128K would have to be saved in order to preserve all the memory potentially used by text mode.

@@ -231,16 +234,20 @@ Code ends end Start
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/28-01.html b/28-01.html index 1b44557..1395ba5 100644 --- a/28-01.html +++ b/28-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 28
Reading VGA Memory

@@ -51,16 +54,20 @@

As you can see, the settings of the Read Map and Map Mask registers for accessing a given plane don’t match. The code in Listing 28.1 illustrates this. Listing 28.1 simply copies a sixteen-color image from system memory to VGA memory, one plane at a time, then animates by repeatedly copying the image back to system memory, again one plane at a time, clearing the old image, and copying the image to a new location in VGA memory. Note the differing settings of the Read Map and Map Mask registers.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/28-02.html b/28-02.html index 72c0c02..31a973f 100644 --- a/28-02.html +++ b/28-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 28.1 L28-1.ASM

 ; Program to illustrate the use of the Read Map register in read mode 0.
@@ -269,16 +272,20 @@ code  ends
      end  Start
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/28-03.html b/28-03.html index ff97191..39c9b5e 100644 --- a/28-03.html +++ b/28-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

By the way, the code in Listing 28.1 is intended only to illustrate read mode 0, and is, in general, a poor way to perform animation, since it’s slow and tends to flicker. Later in this book, we’ll take a look at some far better VGA animation techniques.

As you’d expect, neither the read mode nor the setting of the Read Map register affects CPU writes to VGA memory in any way.

@@ -52,16 +55,20 @@

That’s certainly interesting, but what’s read mode 1 good for? One obvious application is in implementing flood-fill algorithms, since read mode 1 makes it easy to tell when a given byte contains a pixel of a boundary color. Another application is in detecting on-screen object collisions, as illustrated by the code in Listing 28.2.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/28-04.html b/28-04.html index 632ee75..3dc53d8 100644 --- a/28-04.html +++ b/28-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 28.2 L28-2.ASM

 ; Program to illustrate use of read mode 1 (color compare mode)
@@ -209,16 +212,20 @@ end  Start
 
   

Why? Well, as I’ve said, when all planes are “don’t care” planes, read mode 1 reads always return 0FFH. Now, when you AND any value with 0FFH, the value remains unchanged, and that can be awfully handy when you’re using the bit mask to modify selected pixels in VGA memory. Recall that you must always read VGA memory to load the latches before writing to VGA memory when you’re using the bit mask. Traditionally, two separate instructions—a read followed by a write—are used to perform this task. The code in Listing 28.2 uses this approach. Suppose, however, that you’ve set the VGA to read mode 1, with the Color Don’t Care register set to 0 (meaning all reads of VGA memory will return 0FFH). Under these circumstances, you can use a single AND instruction to both read and write VGA memory, since ANDing any value with 0FFH leaves that value unchanged.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/28-05.html b/28-05.html index 5501705..8625167 100644 --- a/28-05.html +++ b/28-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 28.3 illustrates an efficient use of write mode 3 in conjunction with read mode 1 and a Color Don’t Care register setting of 0. The mask in AL is passed directly to the VGA’s bit mask (that’s how write mode 3 works—see Chapter 4 for details). Because the VGA always returns 0FFH, the single AND instruction loads the latches, and writes the value in AL, unmodified, to the VGA, where it is used to generate the bit mask. This is more compact and register-efficient than using separate instructions to read and write, although it is not necessarily faster by cycle count, because on a 486 or a Pentium MOV is a 1-cycle instruction, but AND with memory is a 3-cycle instruction. However, given display memory wait states, it is often the case that the two approaches run at the same speed, and the register that the above approach frees up can frequently be used to save one or more cycles in any case.

By the way, Listing 28.3 illustrates how write mode 3 can make for excellent pixel- and line-drawing code.

@@ -147,16 +150,20 @@ code ends

Not to worry—that still leaves us a slew of interesting VGA topics, including smooth panning and scrolling, the split screen, color selection, page flipping, and Mode X. And that’s not to mention actual uses to which the VGA’s hardware can be put, including lines, circles, polygons, and my personal favorite, animation. We’ve covered a lot of challenging and rewarding ground—and we’ve only just begun.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/29-01.html b/29-01.html index b936741..9721eea 100644 --- a/29-01.html +++ b/29-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 29
Saving Screens and Other VGA Mysteries

@@ -186,16 +189,20 @@ Code ends end Start
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/29-02.html b/29-02.html index 6b33768..25cce96 100644 --- a/29-02.html +++ b/29-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 29.2 L29-2.ASM

 ; Program to restore a mode 10h EGA graphics screen from
@@ -152,16 +155,20 @@ Code          ends
 
   

Screen saving and restoring is pretty simple, eh? There are a few caveats, of course, but nothing serious. First, the adapter’s registers must be programmed properly in order for screen saving and restoring to work. For screen saving, you must be in read mode 0; if you’re in color compare mode, there’s no telling what bit pattern you’ll save, but it certainly won’t be the desired screen image. For screen restoring, you must be in write mode 0, with the Bit Mask register set to 0FFH and Data Rotate register set to 0 (no data rotation and the logical function set to pass the data through unchanged).

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/29-03.html b/29-03.html index f0a2b17..f462487 100644 --- a/29-03.html +++ b/29-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- @@ -74,16 +77,20 @@ DISPLAYED_SCREEN_SIZEequ(640/8)*480


Figure 29.2
  Color translation via the palette registers.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/29-04.html b/29-04.html index 366ac02..040b38c 100644 --- a/29-04.html +++ b/29-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

How does one go about setting the palette registers? Well, it’s certainly possible to set the palette registers directly by addressing them at registers 0 through 0FH of the Attribute Controller. However, setting the palette registers is a bit tricky—bit 5 of the Attribute Controller Index register must be 0 while the palette registers are written to, and glitches can occur if the updating doesn’t take place during the blanking interval—and besides, it turns out that there’s no need at all to go straight to the hardware on this one. Conveniently, the EGA BIOS provides us with video function 10H, which supports setting either any one palette register or all 16 palette registers (and the overscan register as well) with a single video interrupt.

Video function 10H is invoked by performing an INT 10H with AH set to 10H. If AL is 0 (subfunction 0), then BL contains the number of the palette register to set, and BH contains the value to set that register to. If AL is 1 (subfunction 1), then BH contains the value to set the overscan (border) color to. Finally, if AL is 2 (subfunction 2), then ES:DX points to a 17-byte array containing the values to set palette registers 0-15 and the overscan register to. (For completeness, although it’s unrelated to the palette registers, there is one more subfunction of video function 10H. If AL = 3 (subfunction 3), bit 0 of BL is set to 1 to cause bit 7 of text attributes to select blinking, or set to 0 to cause bit 7 of text attributes to select highreverse video.)

@@ -297,16 +300,20 @@ Code ends end Start
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/29-05.html b/29-05.html index b8dd051..72d3e99 100644 --- a/29-05.html +++ b/29-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Overscan

While we’re at it, I’m going to touch on overscan. Overscan is the color of the border of the display, the rectangular area around the edge of the monitor that’s outside the region displaying active video data but inside the blanking area. The overscan (or border) color can be programmed to any of the 64 possible colors by either setting Attribute Controller register 11H directly or calling video function 10H, subfunction 1.

@@ -131,16 +134,20 @@ Code ends end Start
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/29-06.html b/29-06.html index 93f4b2c..4d7b773 100644 --- a/29-06.html +++ b/29-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Does that do it for color selection? Yes and no. For the EGA, we’ve covered the whole of color selection—but not so for the VGA. The VGA can emulate everything we’ve discussed, but actually performs one 4-bit to 8-bit translation (except in 256-color modes, where all 256 colors are simultaneously available), followed by yet another translation, this one 8-bit to 18-bit. What’s more, the VGA has the ability to flip instantly through as many as 16 16-color sets. The VGA’s color selection capabilities, which are supported by another set of BIOS functions, can be used to produce stunning color effects, as we’ll see when we cover them starting in Chapter 33.

Modifying VGA Registers

@@ -55,16 +58,20 @@ out dx,al ;set write mode 1


Figure 29.4
  Graphics mode register fields.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/30-01.html b/30-01.html index 57f5dcb..a88bb17 100644 --- a/30-01.html +++ b/30-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 30
Video Est Omnis Divisa

@@ -58,7 +61,7 @@

Turning the split screen on involves nothing more than setting all bits of the split screen start scan line to the scan line after which you want the split screen to start appearing. (Of course, you’ll probably want to change the start address before using the split screen; otherwise, you’ll just end up displaying the memory at offset zero twice: once in the normal screen and once in the split screen.) Turning off the split screen is a simple matter of setting the split screen start scan line to a value equal to or greater than the last scan line displayed; the safest such approach is to set all bits of the split screen start scan line to 1. (That is, in fact, the split screen start scan line value programmed by the BIOS during a mode set.)

-

The Split Screen in Action

+

The Split Screen in Action

All of these points are illustrated by Listing 30.1. Listing 30.1 fills display memory starting at offset zero (the split screen area of memory) with text identifying the split screen, fills display memory starting at offset 8000H with a graphics pattern, and sets the start address to 8000H. At this point, the normal screen is being displayed (the split screen start scan line is still set to the BIOS default setting, with all bits equal to 1, so the split screen is off), with the pixels based on the contents of display memory at offset 8000H. The contents of display memory between offset 0 and offset 7FFFH are not visible at all.

@@ -66,16 +69,20 @@

Listing 30.1 isn’t done just yet, however. After a keypress, Listing 30.1 demonstrates how to turn the split screen off (by setting all bits of the split screen start scan line to 1). After another keypress, Listing 30.1 shows that the split screen can never cover the whole screen, by setting the start address to 0 and then flipping back and forth between the normal screen and the split screen with a split screen start scan line setting of zero. Both the normal screen and the split screen display the same text, but the split screen displays it one scan line lower, because the split screen doesn’t start until after the first scan line, and that produces a jittering effect as the program switches the split screen on and off. (On the EGA, the split screen may display two scan lines lower, for reasons I’ll discuss shortly.)

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/30-02.html b/30-02.html index a94d1ac..d4a821c 100644 --- a/30-02.html +++ b/30-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Finally, after another keypress, Listing 30.1 halts.

LISTING 30.1 L30-1.ASM

@@ -408,16 +411,20 @@ Code ends end Start
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/30-03.html b/30-03.html index 49208c4..7c98d5f 100644 --- a/30-03.html +++ b/30-03.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

VGA and EGA Split-Screen Operation Don’t Mix

+

VGA and EGA Split-Screen Operation Don’t Mix

You must set the IS_VGA equate at the start of Listing 30.1 correctly for the adapter the code will run on in order for the program to perform properly. This equate determines how the upper bits of the split screen start scan line are set by SetSplitScreenRow. If IS_VGA is 0 (specifying an EGA target), then bit 8 of the split screen start scan line is set by programming the entire Overflow register to 1FH; this is hard-wired for the 350-scan-line modes of the EGA. If IS_VGA is 1 (specifying a VGA target), then bits 8 and 9 of the split screen start scan line are set by reading the registers they reside in, changing only the split-screen-related bits, and writing the modified settings back to their respective registers.

@@ -74,16 +77,20 @@

There’s another respect in which the EGA is inferior to the VGA when it comes to the split screen, and that’s in the area of panning when the split screen is on. This isn’t a bug—it’s just one of the many areas in which the VGA’s designers learned from the shortcomings of the EGA and went the EGA one better.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/30-04.html b/30-04.html index 86162cb..59eab4f 100644 --- a/30-04.html +++ b/30-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Split Screen and Panning

Back in Chapter 23, I presented a program that performed smooth horizontal panning. Smooth horizontal panning consists of two parts: byte-by-byte (8-pixel) panning by changing the start address and pixel-by-pixel intrabyte panning by setting the Pel Panning register (AC register 13H) to adjust alignment by 0 to 7 pixels. (IBM prefers its own jargon and uses the word “pel” instead of “pixel” in much of their documentation, hence “pel panning.” Then there’s DASD, a.k.a. Direct Access Storage Device—IBM-speak for hard disk.)

@@ -48,22 +51,26 @@

On the VGA, there is recourse. A VGA-only bit, bit 5 of the AC Mode Control register (AC register 10H), turns off pel panning in the split screen. In other words, when this bit is set to 1, pel panning is reset to zero before the first line of the split screen, and remains zero until the end of the frame. This doesn’t allow you to pan the split screen horizontally, mind you—there’s no way to do that—but it does let you pan the normal screen while the split screen stays rock-solid. This can be used to produce an attractive “streaming tape” effect in the normal screen while the split screen is used to display non-moving information.

-

The Split Screen and Horizontal Panning: An Example

+

The Split Screen and Horizontal Panning: An Example

Listing 30.2 illustrates the interaction of horizontal smooth panning with the split screen, as well as the suppression of pel panning in the split screen. Listing 30.2 creates a virtual screen 1024 pixels across by setting the Offset register (CRTC register 13H) to 64, sets the normal screen to scan video data beginning far enough up in display memory to leave room for the split screen starting at offset zero, turns on the split screen, and fills in the normal screen and split screen with distinctive patterns. Next, Listing 30.2 pans the normal screen horizontally without setting bit 5 of the AC Mode Control register to 1. As you’d expect, the split screen jerks about quite horribly. After a key press, Listing 30.2 sets bit 5 of the Mode Control register and pans the normal screen again. This time, the split screen doesn’t budge an inch—if the code is running on a VGA.

By the way, if IS_VGA is set to 0 in Listing 30.2, the program will assemble in a form that will run on the EGA and only the EGA. Pel panning suppression in the split screen won’t work in this version, however, because the EGA lacks the capability to support that feature. When the EGA version runs, the split screen simply jerks back and forth during both panning sessions.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/30-05.html b/30-05.html index 01672c1..3af12b2 100644 --- a/30-05.html +++ b/30-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 30.2 L30-2.ASM

 ; Demonstrates the interaction of the split screen and
@@ -452,16 +455,20 @@ Codeends
 endStart
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/30-06.html b/30-06.html index 0886161..4f649a2 100644 --- a/30-06.html +++ b/30-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Notes on Setting and Reading Registers

There are a few interesting points regarding setting and reading registers to be made about Listing 30.2. First, bit 5 of the AC Index register should be set to 1 whenever palette RAM is not being set (which is to say, all the time in your code, because palette RAM should normally be set via the BIOS). When bit 5 is 0, video data from display memory is no longer sent to palette RAM, and the screen becomes a solid color—not normally a desirable state of affairs.

@@ -56,16 +59,20 @@

What if you wanted to pan faster? Well, you could of course just move two pixels at a time rather than one; I assure you no one will ever notice when you’re panning at a rate of 10 or more times per second.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/30-07.html b/30-07.html index dd4d51c..0469a8d 100644 --- a/30-07.html +++ b/30-07.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Split Screens in Other Modes

So far we’ve only discussed the split screen in mode 10H. What about other modes? Generally, the split screen works in any mode; the basic rule is that when a scan line on the screen matches the split screen scan line, the internal display memory pointer is reset to zero. I’ve found this to be true even in oddball modes, such as line-doubled CGA modes and the 320x200 256-color mode (which is really a 320x400 mode with each line repeated. For split-screen purposes, the VGA and EGA seem to count purely in scan lines, not in rows or doubled scan lines or the like. However, I have run into small anomalies in those modes on clones, and I haven’t tested all modes (nor, lord knows, all clones!) so be careful when using the split screen in modes other than modes 0DH-12H, and test your code on a variety of hardware.

@@ -46,16 +49,20 @@

In short, use the fancy stuff—but only when you have

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/31-01.html b/31-01.html index e6a65ec..6a8994d 100644 --- a/31-01.html +++ b/31-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 31
Higher 256-Color Resolution on the VGA

@@ -63,20 +66,24 @@

That’s why I like 320x400 256-color mode. The next step is to understand how display memory is organized in 320x400 mode, and that’s not so simple.

-

Display Memory Organization in 320x400 Mode

+

Display Memory Organization in 320x400 Mode

First, let’s look at why display memory must be organized differently in 320x400 256-color mode than in mode 13H. The designers of the VGA intentionally limited the maximum size of the bitmap in mode 13H to 64K, thereby limiting resolution to 320x200. This was accomplished in hardware, so there is no way to extend the bitmap organization of mode 13H to 320x400 mode.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/31-02.html b/31-02.html index b9808ca..f861f0e 100644 --- a/31-02.html +++ b/31-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

That’s a shame, because mode 13H has the simplest bitmap organization of any mode—one long, linear bitmap, with each byte controlling one pixel. We can’t have that organization, though, so we’ll have to find an acceptable substitute if we want to use a higher 256-color resolution.

We’re talking about the VGA, so of course there are actually several bitmap organizations that let us use higher 256-color resolutions than mode 13H. The one I like best is shown in Figure 31.1. Each byte controls one 256-color pixel. Pixel 0 is at address 0 in plane 0, pixel 1 is at address 0 in plane 1, pixel 2 is at address 0 in plane 2, pixel 3 is at address 0 in plane 3, pixel 4 is at address 1 in plane 0, and so on.

@@ -55,22 +58,26 @@

Our next task is to convert standard mode 13H into 320x400 mode. That’s accomplished by undoing some of the mode bits that are set up especially for mode 13H, so that from a programming perspective the VGA reverts to a straightforward planar model of memory. That means taking the VGA out of chain 4 mode and doubleword mode, turning off the double display of each scan line, making sure chain mode, odd/even mode, and word mode are turned off, and selecting byte mode for video data display. All that’s done in the Set320x400Mode subroutine in Listing 31.1, which we’ll discuss next.

-

Reading and Writing Pixels

+

Reading and Writing Pixels

The basic graphics functions in any mode are functions to read and write single pixels. Any more complex function can be built on these primitives, although that’s rarely the speediest solution. What’s more, once you understand the operation of the read and write pixel functions, you’ve got all the knowledge you need to create functions that perform more complex graphics functions. Consequently, we’ll start our exploration of 320x400 mode with pixel-at-a-time line drawing.

Listing 31.1 draws 8 multicolored octagons in turn, drawing a new one on top of the old one each time a key is pressed. The main-loop code of Listing 31.1 should be easily understood; a series of diagonal, horizontal, and vertical lines are drawn one pixel at a time based on a list of line descriptors, with the draw colors incremented for each successive time through the line list.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/31-03.html b/31-03.html index 9dd0470..8ccc667 100644 --- a/31-03.html +++ b/31-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 31.1 L31-1.ASM

 ; Program to demonstrate pixel drawing in 320x400 256-color
@@ -361,16 +364,20 @@ Code   ends
 end    Start
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/31-04.html b/31-04.html index c33020c..4c62d75 100644 --- a/31-04.html +++ b/31-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The interesting aspects of Listing 31.1 are three. First, the Set320x400Mode subroutine selects 320x400 256-color mode. This is accomplished by performing a mode 13H mode set followed by then putting the VGA into standard planar byte mode. Set320x400Mode zeros display memory as well. It’s necessary to clear display memory even after a mode 13H mode set because the mode 13H mode set clears only the 64K of display memory that can be accessed in that mode, leaving 192K of display memory untouched.

The second interesting aspect of Listing 31.1 is the WritePixel subroutine, which draws a colored pixel at any x,y addressable location on the screen. Although it may not be obvious because I’ve optimized the code a little, the process of drawing a pixel is remarkably simple. First, the pixel’s display memory address is calculated as

@@ -66,16 +69,20 @@

The color bar routines don’t use the WritePixel subroutine from Listing 31.1; they go straight to display memory instead for improved speed. As I mentioned above, better speed yet could be achieved by a color-bar algorithm that draws all the pixels in plane 0, then all the pixels in plane 1, and so on, thereby avoiding the overhead of constantly reprogramming the Map Mask register.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/31-05.html b/31-05.html index 9fdc4f0..5b00b3e 100644 --- a/31-05.html +++ b/31-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 31.2 L31-2.ASM

 ; Program to demonstrate the two pages available in 320x400
@@ -299,16 +302,20 @@ endStart
 
   

You can, if you wish, use the display memory organization of 320x400 mode in 320x200 mode by modifying Set320x400Mode to leave the maximum scan line setting at 1 in the mode set. (The version of Set320x400Mode in Listings 31.1 and 31.2 forces the maximum scan line to 0, doubling the effective resolution of the screen.) Why would you want to do that? For one thing, you could then choose from not two but four 320x200 256-color display pages, starting at offsets 0, 4000H, 8000H, and 0C000H in display memory. For another, having only half as many pixels per screen can as much as double drawing speeds; that’s one reason that many games run at 320x200, and even then often limit the active display drawing area to only a portion of the screen.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/32-01.html b/32-01.html index 6d081d8..73cac80 100644 --- a/32-01.html +++ b/32-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 32
Be It Resolved: 360x480

@@ -65,16 +68,20 @@

Listing 32.2 contains an adaptation of some C linecode I’ll be presenting shortly in Chapter 35. If you’re reading this book in serial fashion and haven’t gotten there yet, simply take it on faith. If you really really need to know how the line-draw code works right now, by all means make a short forward call to Chapter 35 and digest it. The line-draw code presented below has been altered to select 360x480 256-color mode, and to cycle through all 256 colors that this mode supports, drawing each line in a different color.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/32-02.html b/32-02.html index 30ac73b..55508ef 100644 --- a/32-02.html +++ b/32-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 32.1 L32-1.ASM

 ; Borland C/C++ tiny/small/medium model-callable assembler
@@ -245,16 +248,20 @@ _TEX   Tends
        end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/32-03.html b/32-03.html index b961507..aaf219b 100644 --- a/32-03.html +++ b/32-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 32.2 L32-2.C

  * Sample program to illustrate VGA line drawing in 360x480
@@ -244,16 +247,20 @@ void main()
     
   
 
-  


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/32-04.html b/32-04.html index 4332f2d..790ab46 100644 --- a/32-04.html +++ b/32-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The second thing you’ll notice is that exquisite shading effects are possible in 360x480 256-color mode; adjacent lines blend together remarkably smoothly, even with the default palette. The VGA allows you to select your 256 colors from a palette of 256K, so you could, if you wished, set up the colors to produce still finer shading albeit with fewer distinctly different colors available. For more on this and related topics, see the coverage of palette reprogramming that begins in the next chapter.

The one thing you may not notice right away is just how much detail is visible on the screen, because the blending of colors tends to obscure the superior resolution of this mode. Each of the four rectangles displayed measures 180 pixels horizontally by 240 vertically. Put another way, each one of those rectangles has two-thirds as many pixels as the entire mode 13H screen; in all, 360x480 256-color mode has 2.7 times as many pixels as mode 13H! As mentioned above, the resolution is unevenly distributed, with vertical resolution matching that of mode 12H but horizontal resolution barely exceeding that of mode 13H—but resolution is hot stuff, no matter how it’s laid out, and 360x480 256-color mode has the highest 256-color resolution you’re ever likely to see on a standard VGA. (SuperVGAs are quite another matter—but when you require a SuperVGA you’re automatically excluding what might be a significant chunk of the market for your code.)

@@ -42,28 +45,32 @@

360x480 256-color mode is essentially 320x400 256-color mode, but stretched in both dimensions. Let’s look at the vertical stretching first, since that’s the simpler of the two.

-

480 Scan Lines per Screen: A Little Slower, But No Big Deal

+

480 Scan Lines per Screen: A Little Slower, But No Big Deal

There’s nothing unusual about 480 scan lines; standard modes 11H and 12H support that vertical resolution. The number of scan lines has nothing to do with either the number of colors or the horizontal resolution, so converting 320x400 256mode to 320x480 256-color mode is a simple matter of reprogramming the VGA’s vertical control registers—which control the scan lines displayed, the vertical sync pulse, vertical blanking, and the total number of scan lines—to the 480-scansettings, and setting the polarities of the horizontal and vertical sync pulses to tell the monitor to adjust to a 480-line screen.

Switching to 480 scan lines has the effect of slowing the screen refresh rate. The VGA always displays at 70 Hz except in 480-scan-line modes; there, due to the time required to scan the extra lines, the refresh rate slows to 60 Hz. (VGA monitors always scan at the same rate horizontally; that is, the distance across the screen covered by the electron beam in a given period of time is the same in all modes. Consequently, adding extra lines per frame requires extra time.) 60 Hz isn’t bad—that’s the only refresh rate the EGA ever supported, and the EGA was the industry standard in its time—but it does tend to flicker a little more and so is a little harder on the eyes than 70 Hz.

-

360 Pixels per Scan Line: No Mean Feat

+

360 Pixels per Scan Line: No Mean Feat

Converting from 320 to 360 pixels per scan line is more difficult than converting from 400 to 480 scan lines per screen. None of the VGA’s graphics modes supports 360 pixels across the screen, or anything like it; the standard choices are 320 and 640 pixels across. However, the VGA does support the horizontal resolution we seek—360 pixels—in 40-column text mode.

Unfortunately, the register settings that select those horizontal resolutions aren’t directly transferable to graphics mode. Text modes display 9 dots (the width of one character) for each time information is fetched from display memory, while graphics modes display just 4 or 8 dots per display memory fetch. (Although it’s a bit confusing, it’s standard terminology to refer to the interval required for one display memory fetch as a “character,” and I’ll follow that terminology from now on.) Consequently, both modes display either 40 or 80 characters per scan line; the only difference is that text modes display more pixels per character. Given that graphics modes can’t display 9 dots per character (there’s only enough information for eight 16pixels or four 256-color pixels in each memory fetch, and that’s that), we’d seem to be at an impasse.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/32-05.html b/32-05.html index 7c95d3d..0a9aff3 100644 --- a/32-05.html +++ b/32-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The key to solving this problem lies in recalling that the VGA is designed to drive a monitor that sweeps the electron beam across the screen at exactly the same speed, no matter what mode the VGA is in. If the monitor always sweeps at the same speed, how does the VGA manage to display both 640 pixels across the screen (in high-resolution graphics modes) and 720 pixels across the screen (in 80-column text modes)? Good question indeed—and the answer is that the VGA has not one but two clocks on board, and one of those clocks is just sufficiently faster than the other clock so that an extra 80 (or 40) pixels can be displayed on each scan line.

In other words, there’s a slow clock (about 25 MHz) that’s usually used in graphics modes to get 640 (or 320) pixels on the screen during each scan line, and a second, fast clock (about 28 MHz) that’s usually used in text modes to crank out 720 (or 360) pixels per scan line. In particular, 320x400 256-color mode uses the 25 MHz clock.

@@ -40,7 +43,7 @@

Once all that’s done, the VGA is in 360x480 mode, awaiting our every high-resolution 256-color graphics whim.

-

Accessing Display Memory in 360x480 256-Color Mode

+

Accessing Display Memory in 360x480 256-Color Mode

Setting up for 360x480 256-color mode proved to be quite a task. Is drawing in this mode going to be as difficult?

@@ -65,16 +68,20 @@

There’s more and better to come, though; in later chapters, we’ll return to high-resolution 256-color programming in a big way, by exploring the tremendous potential of these modes for real time 2-D and 3-D animation.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/33-01.html b/33-01.html index d3b7f02..8f89ba5 100644 --- a/33-01.html +++ b/33-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 33
Yogi Bear and Eurythmics Confront VGA Colors

@@ -49,13 +52,13 @@

Briefly put, the VGA color translation circuitry takes in one 4- or 8-bit pixel value at a time and translates it into three 6-bit values, one each of red, green, and blue, that are converted to corresponding analog levels and sent to the monitor. Seems simple enough, doesn’t it? Unfortunately, nothing is ever that simple on the VGA, and color translation is no exception.

-

The Palette RAM

+

The Palette RAM

The color path in the VGA involves two stages, as shown in Figure 33.1. The first stage fetches a 4-bit pixel from display memory and feeds it into the EGA-compatible palette RAM (so called because it is functionally equivalent to the palette RAM color translation circuitry of the EGA), which translates it into a 6-bit value and sends it on to the DAC. The translation involves nothing more complex than the 4-bit value of a pixel being used as the address of one of the 16 palette RAM registers; a pixel value of 0 selects the contents of palette RAM register 0, a pixel value of 1 selects register 1, and so on. Each palette RAM register stores 6 bits, so each time a palette RAM register is selected by an incoming 4-bit pixel value, 6 bits of information are sent out by the palette RAM. (The operation of the palette RAM was described back in Chapter 29.)

The process is much the same in text mode, except that in text mode each 4-bit pixel value is generated based on the character’s font pattern and attribute. In 256-color mode, which we’ll get to eventually, the palette RAM is not a factor from the programmer’s perspective and should be left alone.

-

The DAC

+

The DAC

Once the EGA-compatible palette RAM has fulfilled its karma and performed 4-bit to 6-bit translation on a pixel, the resulting value is sent to the DAC (Digital/Analog Converter). The DAC performs an 8-bit to 18-bit conversion in much the same manner as the palette RAM, converts the 18-bit result to analog red, green, and blue signals (6 bits for each signal), and sends the three analog signals to the monitor. The DAC is a separate chip, external to the VGA chip, but it’s an integral part of the VGA standard and is present on every VGA.

@@ -66,16 +69,20 @@

The DAC contains 256 18-bit storage registers, used to translate one of 256 possible 8-bit values into one of 256K (262,144, to be precise) 18-bit values. The 18-bit values are actually composed of three 6-bit values, one each for red, green, and blue; for each color component, the higher the number, the brighter the color, with 0 turning that color off in the pixel and 63 (3FH) making that color maximum brightness. Got all that?

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/33-02.html b/33-02.html index 47f236f..52f4c8b 100644 --- a/33-02.html +++ b/33-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Color Paging with the Color Select Register

+

Color Paging with the Color Select Register

“Wait a minute,” you say bemusedly. “Aren’t you missing some bits between the palette RAM and the DAC?” Indeed I am. The palette RAM puts out 6 bits at a time, and the DAC takes in 8 bits at a time. The two missing bits—bits 6 and 7 going into the DAC—are supplied by bits 2 and 3 of the Color Select register (Attribute Controller register 14H). This has intriguing implications. In 16-color modes, pixel data can select only one of 16 attributes, which the EGA palette RAM translates into one of 64 attributes. Normally, those 64 attributes look up colors from registers 0 through 63 in the DAC, because bits 2 and 3 of the Color Select register are both zero. By changing the Color Select register, however, one of three other 64 color sets can be selected instantly. I’ll refer to the process of flipping through color sets in this manner as color paging.

@@ -38,7 +41,7 @@

Why is it a good idea to set the palette RAM to a pass-through state? It’s a good idea because the palette RAM is programmed by the BIOS to EGA-compatible settings and the first 64 DAC registers are programmed to emulate the 64 colors that an EGA can display during mode sets for 16-color modes. This is done for compatibility with EGA programs, and it’s useless if you’re going to tinker with the VGA’s colors. As a VGA programmer, you want to take a 4-bit pixel value and turn it into an 18-bit RGB value; you can do that without any help from the palette RAM, and setting the palette RAM to pass-through values effectively takes it out of the circuit and simplifies life something wonderful. The palette RAM exists solely for EGA compatibility, and serves no useful purpose that I know of for VGA-only color programming.

-

256-Color Mode

+

256-Color Mode

So far I’ve spoken only of 16-color modes; what of 256-color modes?

@@ -46,7 +49,7 @@

On the other hand, feel free to alter the DAC settings to your heart’s content in 256-color mode, all the more so because this is the only mode in which all 256 DAC settings can be displayed simultaneously. By the way, the Color Select register and bit 7 of the Attribute Controller Mode register are ignored in 256-color mode; all 8 bits sent from the VGA chip to the DAC come from display memory. Therefore, there is no color paging in 256-color mode. Of course, that makes sense given that all 256 DAC registers are simultaneously in use in 256-color mode.

-

Setting the Palette RAM

+

Setting the Palette RAM

The palette RAM can be programmed either directly or through BIOS interrupt 10H, function 10H. I strongly recommend using the BIOS interrupt; a clone BIOS may mask incompatibilities with genuine IBM silicon. Such incompatibilities could include anything from flicker to trashing the palette RAM; or they may not exist at all, but why find out the hard way? My policy is to use the BIOS unless there’s a clear reason not to do so, and there’s no such reason that I know of in this case.

@@ -56,16 +59,20 @@

Having said that, let’s leave the palette RAM behind (presumably in a pass-through state) and move on to the DAC, which is the right place to do color translation on the VGA.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/33-03.html b/33-03.html index 790c21c..9e01ebc 100644 --- a/33-03.html +++ b/33-03.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Setting the DAC

+

Setting the DAC

Like the palette RAM, the DAC registers can be set either directly or through the BIOS. Again, the BIOS should be used whenever possible, but there are a few complications here. My experience is that varying degrees of flicker and screen bounce occur on many VGAs when a large block of DAC registers is set through the BIOS. That’s not a problem when the DAC is loaded just once and then left that way, as is the case in Listing 33.1, which we’ll get to shortly, but it can be a serious problem when the color set is changed rapidly (“cycled”) to produce on-screen effects such as rippling colors. My (limited) experience is that it’s necessary to program the DAC directly in order to cycle colors cleanly, although input from readers who have worked extensively with VGA color is welcome.

@@ -60,16 +63,20 @@

This chapter has gotten about as big as a chapter really ought to be; the VGA color saga will continue in the next few. Quickly, then, Listing 33.1 is a simple example of setting the DAC that gives you a taste of the spectacular effects that color translation makes possible. There’s nothing particularly complex about Listing 33.1; it just selects 256-color mode, fills the screen with one-pixel-wide concentric diamonds drawn with sequential attributes, and sets the DAC to produce a smooth gradient of each of the three primary colors and of a mix of red and blue. Run the program; I suspect you’ll be surprised at the stunning display this short program produces. Clever color manipulation is perhaps the easiest way to produce truly eye-catching effects on the PC.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/33-04.html b/33-04.html index f9ff64e..10c130d 100644 --- a/33-04.html +++ b/33-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 33.1 L33-1.ASM

 ; Program to demonstrate use of the DAC registers by selecting a
@@ -215,16 +218,20 @@ FillVertLoop:
 
   

In this chapter, we traced the surprisingly complex path by which the VGA turns a pixel value into RGB analog signals headed for the monitor. In the next chapter and Chapter A on the companion CD-ROM, we’ll look at some more code that plays with VGA color. We’ll explore in more detail the process of reading and writing the palette RAM and DAC registers, and we’ll observe color paging and cycling in action.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/34-01.html b/34-01.html index 6f8cbe4..0d3ede1 100644 --- a/34-01.html +++ b/34-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 34
Changing Colors without Writing Pixels

@@ -59,16 +62,20 @@

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.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/34-02.html b/34-02.html index dd87c94..e7db7f2 100644 --- a/34-02.html +++ b/34-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Loading the DAC via 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.

@@ -50,7 +53,7 @@

Which is not to say that loading the DAC directly is a picnic either, as we’ll see next.

-

Loading the DAC Directly

+

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 OUTs 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 OUTs in all.

@@ -60,16 +63,20 @@

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!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/34-03.html b/34-03.html index 39d4587..88ddb74 100644 --- a/34-03.html +++ b/34-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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.

@@ -289,16 +292,20 @@ endif;USE_BIOS endstart
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/34-04.html b/34-04.html index 525afc6..ab7a66f 100644 --- a/34-04.html +++ b/34-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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.

@@ -68,16 +71,20 @@ -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/34-05.html b/34-05.html index ccfeb31..939e0c0 100644 --- a/34-05.html +++ b/34-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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.

@@ -44,11 +47,11 @@

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

+

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

+

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.

@@ -58,20 +61,24 @@

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

+

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?”

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/35-01.html b/35-01.html index b315654..cc4c437 100644 --- a/35-01.html +++ b/35-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 35
Bresenham Is Fast, and Fast Is Good

@@ -70,16 +73,20 @@


Figure 35.1
  Approximating a true line from a pixel array.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/35-02.html b/35-02.html index 1ece3c7..dba9093 100644 --- a/35-02.html +++ b/35-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Let’s examine the case of drawing a line where the horizontal, or X length of the line is greater than the vertical, or Y length, and both lengths are greater than 0. For example, suppose we are drawing a line from (0,0) to (5,2), as shown in Figure 35.2. Note that Figure 35.2 shows the upper-left-hand corner of the screen as (0,0), rather than placing (0,0) at its more traditional lower-left-hand corner location. Due to the way in which the PC’s graphics are mapped to memory, it is simpler to work within this framework, although a translation of Y from increasing downward to increasing upward could be effected easily enough by simply subtracting the Y coordinate from the screen height minus 1; if you are more comfortable with the traditional coordinate system, feel free to modify the code in Listings 35.1 and 35.3.

In Figure 35.2, the endpoints of the line fall exactly on displayed pixels. However, no other part of the line squarely intersects the center of a pixel, meaning that all other pixels will have to be plotted as approximations of the line. The approach to approximation that Bresenham’s algorithm takes is to move exactly 1 pixel along the major dimension of the line each time a new pixel is drawn, while moving 1 pixel along the minor dimension each time the line moves more than halfway between pixels along the minor dimension.

@@ -60,7 +63,7 @@

The above discussion summarizes the nature rather than the exact mechanism of Bresenham’s line-drawing algorithm. I’ll provide a brief seat-of-the-pants discussion of the algorithm in action when we get to the C implementation of the algorithm; for a full mathematical treatment, I refer you to pages 433-436 of Foley and Van Dam’s Fundamentals of Interactive Computer Graphics (Addison-Wesley, 1982), or pages 72-78 of the second edition of that book, which was published under the name Computer Graphics: Principles and Practice (Addison-Wesley, 1990). These sources provide the derivation of the integer-only, divide-free version of the algorithm, as well as Pascal code for drawing lines in one of the eight possible octants.

-

Strengths and Weaknesses

+

Strengths and Weaknesses

The overwhelming strength of Bresenham’s line-drawing algorithm is speed. With no divides, no floating-point operations, and no need for variables that won’t fit in 16 bits, it is perfectly suited for PCs.

@@ -68,16 +71,20 @@

Then, too, users hate waiting for their computer to finish drawing. By any standard of drawing performance, Bresenham’s algorithm excels.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/35-03.html b/35-03.html index 250d8f8..4da0791 100644 --- a/35-03.html +++ b/35-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

An Implementation in C

It’s time to get down and look at some actual working code. Listing 35.1 is a C implementation of Bresenham’s line-drawing algorithm for modes 0EH, 0FH, 10H, and 12H of the VGA, called as function EVGALine. Listing 35.2 is a sample program to demonstrate the use of EVGALine.

@@ -228,16 +231,20 @@ char Color; /* color to draw line in */ }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/35-04.html b/35-04.html index 85c2e21..9aad8c5 100644 --- a/35-04.html +++ b/35-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 35.2 L35-2.C

 /*
@@ -117,7 +120,7 @@ void main()
 }
 
-

Looking at EVGALine

+

Looking at EVGALine

The EVGALine function itself performs four operations. EVGALine first sets up the VGA’s hardware so that all pixels drawn will be in the desired color. This is accomplished by setting two of the VGA’s registers, the Enable Set/Reset register and the Set/Reset register. Setting the Enable Set/Reset to the value 0FH, as is done in EVGALine, causes all drawing to produce pixels in the color contained in the Set/Reset register. Setting the Set/Reset register to the passed color, in conjunction with the Enable Set/Reset setting of 0FH, causes all drawing done by EVGALine and the functions it calls to generate the passed color. In summary, setting up the Enable Set/Reset and Set/Reset registers in this way causes the remainder of EVGALine to draw a line in the specified color.

@@ -138,16 +141,20 @@ void main()


Figure 35.4
  Bresenham’s eight possible line orientations.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/35-05.html b/35-05.html index 793920d..807e438 100644 --- a/35-05.html +++ b/35-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

There is one line-drawing function for octants 0 and 3, Octant0, and one line-drawing function for octants 1 and 2, Octant1. A single function with if statements could certainly be used to handle all four octants, but at a significant performance cost. There is, on the other hand, very little performance cost to grouping octants 0 and 3 together and octants 1 and 2 together, since the two octants in each pair differ only in the direction of change of the X coordinate.

EVGALine determines which line-drawing function to call and with what value for the direction of change of the X coordinate based on two criteria: whether DeltaX is negative or not, and whether the absolute value of DeltaX (|DeltaX|) is less than DeltaY or not, as shown in Figure 35.5. Recall that the value of DeltaY, and hence the direction of change of the Y coordinate, is guaranteed to be non-negative as a result of the earlier elimination of four of the line orientations.

@@ -39,7 +42,7 @@


Figure 35.5
  EVGALine’s decision logic.

-

Drawing Each Line

+

Drawing Each Line

The Octant0 and Octant1 functions draw lines for which |DeltaX| is greater than DeltaY and lines for which |DeltaX| is less than or equal to DeltaY, respectively. The parameters to Octant0 and Octant1 are the starting point of the line, the length of the line in each dimension, and XDirection, the amount by which the X coordinate should be changed when it moves. XDirection must be either 1 (to draw toward the right edge of the screen) or -1 (to draw toward the left edge of the screen). No value is required for the amount by which the Y coordinate should be changed; since DeltaY is guaranteed to be positive, the Y coordinate always changes by 1 pixel.

@@ -47,7 +50,7 @@

Octant1 draws lines for which |DeltaX| is less than or equal to DeltaY. For these lines, the Y coordinate of each pixel drawn is 1 greater than the Y coordinate of the previous pixel. Whenever ErrorTerm becomes non-negative, indicating that the next X coordinate is a better approximation of the line being drawn, the X coordinate is advanced by either 1 or -1, depending on the value of XDirection. (This makes it possible for Octant1 to draw lines in both octant 1 and octant 2.)

-

Drawing Each Pixel

+

Drawing Each Pixel

At the core of Octant0 and Octant1 is a pixel-drawing function, EVGADot. EVGADot draws a pixel at the specified coordinates in whatever color the hardware of the VGA happens to be set up for. As described earlier, since the entire line drawn by EVGALine is of the same color, line-drawing performance is improved by setting the VGA’s hardware up once in EVGALine before the line is drawn, and then drawing all the pixels in the line in the same color via EVGADot.

@@ -59,16 +62,20 @@

Finally, 0FEH is ORed with the display memory byte controlling the pixel to be drawn. ORing with 0FEH first reads display memory, thereby loading the VGA’s internal latches with the contents of the display memory byte controlling the pixel to be drawn, and then writes to display memory with the value 0FEH. Because of the unusual way in which the VGA’s data paths work and the way in which EVGALine sets up the VGA’s Enable Set/Reset and Set/Reset registers, the value that is written by the OR instruction is ignored. Instead, the value that actually gets placed in display memory is the color that was passed to EVGALine and placed in the Set/Reset register. The Bit Mask register, which was set up in step three above, allows only the single bit controlling the pixel to be drawn to be set to this color value. For more on the various machineries the VGA brings to bear on graphics data, look back to Chapter 25.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/35-06.html b/35-06.html index ac5893f..13a5948 100644 --- a/35-06.html +++ b/35-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The result of all this is simply a single pixel drawn in the color set up in EVGALine. EVGADot may seem excessively complex for a function that does nothing more that draw one pixel, but programming the VGA isn’t trivial (as we’ve seen in the early chapters of this part). Besides, while the explanation of EVGADot is lengthy, the code itself is only five lines long.

Line drawing would be somewhat faster if the code of EVGADot were made an inline part of Octant0 and Octant1, thereby saving the overhead of preparing parameters and calling the function. Feel free to do this if you wish; I maintained EVGADot as a separate function for clarity and for ease of inserting a pixel-drawing function for a different graphics adapter, should that be desired. If you do install a pixel-drawing function for a different adapter, or a fundamentally different mode such as a 256-color SuperVGA mode, remember to remove the hardware-dependent outportb lines in EVGALine itself.

@@ -48,16 +51,20 @@

Listing 35.3 is a high-performance implementation of Bresenham’s algorithm, written entirely in assembly language. The code is callable from C just as is Listing 35.1, with the same name, EVGALine, and with the same parameters. Either of the two can be linked to any program that calls EVGALine, since they appear to be identical to the calling program. The only difference between the two versions is that the sample program in Listing 35.2 runs over three times as fast on a 486 with an ISA-bus VGA when calling the assembly-language version of EVGALine as when calling the C version, and the difference would be considerably greater yet on a local bus, or with the use of write mode 3. Link each version with Listing 35.2 and compare performance—the difference is startling.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/35-07.html b/35-07.html index ee480e5..ec04bc9 100644 --- a/35-07.html +++ b/35-07.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 35.3 L35-3.ASM

 ; Fast assembler implementation of Bresenham’s line-drawing algorithm
@@ -408,16 +411,20 @@ _EVGALine       endp
 
   

All too many PC programmers fall into the high-level-language trap of thinking that a good algorithm guarantees good performance. Not so: As our two implementations of Bresenham’s algorithm graphically illustrate (pun not originally intended, but allowed to stand once recognized), truly great PC code requires both a good algorithm and a good assembly implementation. In Listing 35.3, we’ve got y-oh-my, isn’t it fun?

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/36-01.html b/36-01.html index 7c33818..335321f 100644 --- a/36-01.html +++ b/36-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 36
The Good, the Bad, and the Run-Sliced

@@ -96,16 +99,20 @@ else


Figure 36.1
  Standard Bresenham’s line drawing.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/36-02.html b/36-02.html index 28adf94..78f8922 100644 --- a/36-02.html +++ b/36-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The run-length slice algorithm rotates matters 90 degrees, with salubrious results. The basis of the run-length slice algorithm is stepping one pixel at a time along the minor axis (the shorter dimension), while maintaining an integer error term indicating how close the line is to advancing an extra pixel along the major axis, as illustrated by Figure 36.2.

Consider this: When you’re called upon to draw a line with an X-dimension of 35 and a Y-dimension of 10, you have a great deal of information available, some of which is ignored by standard Bresenham’s. In particular, because the slope is between 1/3 and 1/4, you know that every single run—a run being a set of pixels at the same minor-axis coordinate—must be either three or four pixels long. No other length is possible, as shown in Figure 36.3 (apart from the first and last runs, which are special cases that I’ll discuss shortly). Therefore, for this line, there’s no need to perform an error-term calculation and test for each pixel. Instead, we can just perform one test per run, to see whether the run is three or four pixels long, thereby eliminating about 70 percent of the calculations in drawing this line.

@@ -59,16 +62,20 @@

That’s good.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/36-03.html b/36-03.html index 09c250c..8fbcefa 100644 --- a/36-03.html +++ b/36-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Run-Length Slice Details

A couple of run-length slice implementation details yet remain. First is the matter of how error-term turnover is detected. This is done in much the same way as it is with standard Bresenham’s: The error term is maintained as a negative valve and advances for each step; when the error term reaches 0, it’s time to add an extra pixel to the current run. This means that we only have to test for carry after advancing the error term to determine whether or not to add an extra pixel to each run. (Actually, the code in this chapter tests for the error term being greater than zero, but the assembly code in the next chapter will use the very efficient carry approach.)

@@ -287,16 +290,20 @@ void DrawVerticalRun(char far **ScreenPtr, int XAdvance,
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/36-04.html b/36-04.html index 1d9bce6..9c8e9c8 100644 --- a/36-04.html +++ b/36-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Notwithstanding that it’s not optimized, Listing 36.1 is reasonably fast. If you run Listing 36.2 (a sample line-drawing program that you can use to test-drive Listing 36.1), you may be as surprised as I was at how quickly the screen fills with vectors, considering that Listing 36.1 is entirely in C and has some redundant divides. Or perhaps you won’t be surprised—in which case I suggest you not miss the next chapter.

LISTING 36.2 L36-2.C

@@ -110,16 +113,20 @@ int main() }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/37-01.html b/37-01.html index 6b61fd8..dce5687 100644 --- a/37-01.html +++ b/37-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 37
Dead Cats and Lightning Lines

@@ -403,16 +406,20 @@ _LineDraw endp end
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/37-02.html b/37-02.html index 948ba60..b8ce841 100644 --- a/37-02.html +++ b/37-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

How Fast Is Fast?

+

How Fast Is Fast?

Your first question is likely to be the following: Just how fast is Listing 37.1? Is it optimized to the hilt or just pretty fast? The quick answer is: It’s fast. Listing 37.1 draws lines at a rate of nearly 1 million pixels per second on my 486/33, and is capable of still faster drawing, as I’ll discuss shortly. (The heavily optimized AutoCAD line-drawing code that I mentioned in the last chapter drew 150,000 pixels per second on an EGA in a 386/16, and I thought I had died and gone to Heaven. Such is progress.) The full answer is a more complicated one, and ties in to the principle that if it is broken, maybe that’s okay—and to the principle of looking before you leap, also known as profiling before you optimize.

@@ -52,7 +55,7 @@

Profile before you optimize.

-

Further Optimizations

+

Further Optimizations

Following is a quick tour of some of the many possible further optimizations to Listing 37.1.

@@ -68,16 +71,20 @@

If your code looks broken from a performance perspective, think before you fix it; that particular cat may be dead for a perfectly good reason. I’ll say it again: Profile before you optimize.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/38-01.html b/38-01.html index 69ad6b3..54d7d51 100644 --- a/38-01.html +++ b/38-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 38
The Polygon Primeval

@@ -55,7 +58,7 @@

Why bother to distinguish between convex, nonconvex, and complex polygons? Easy: performance, especially when it comes to filling convex polygons. We’re going to start with filled convex polygons; they’re widely useful and will serve well to introduce some of the subtler complexities of polygon drawing, not the least of which is the slippery concept of “inside.”

-

Which Side Is Inside?

+

Which Side Is Inside?

The basic principle of polygon filling is decomposing each polygon into a series of horizontal lines, one for each horizontal row of pixels, or scan line, within the polygon (a process I’ll call scan conversion), and drawing the horizontal lines. I’ll refer to the entire process as rasterization. Rasterization of convex polygons is easily done by starting at the top of the polygon and tracing down the left and right sides, one scan line (one vertical pixel) at a time, filling the extent between the two edges on each scan line, until the bottom of the polygon is reached. At first glance, rasterization does not seem to be particularly complicated, although it should be apparent that this simple approach is inadequate for nonconvex polygons.

@@ -73,16 +76,20 @@

There’s one great drawback to tracing polygons with standard lines, however: Adjacent polygons won’t fit together properly, as shown in Figure 38.3. If you use six equilateral triangles to make a hexagon, for example, the edges of the triangles will overlap when traced with standard lines, and more recently drawn triangles will wipe out portions of their predecessors. Worse still, odd color effects will show up along the polygon boundaries if XOR drawing is used. Consequently, filling out to the boundary lines just won’t do for drawing images composed of fitted-together polygons. And because fitting polygons together is exactly what I have in mind, we need a different approach.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/38-02.html b/38-02.html index 85413cf..10ea68c 100644 --- a/38-02.html +++ b/38-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

How Do You Fit Polygons Together?

+

How Do You Fit Polygons Together?

How, then, do you fit polygons together? Very carefully. First, the line-tracing algorithm must be adjusted so that it selects only those pixels that are truly inside the polygon. This basically requires shifting a standard line-drawing algorithm horizontally by one half-pixel toward the polygon’s interior. That leaves the issue of how to handle points that are exactly on the boundary, and points that lie at vertices, so that those points are drawn once and only once. To deal with that, we’re going to adopt the following rules:

@@ -324,16 +327,20 @@ }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/38-03.html b/38-03.html index e81982c..522da29 100644 --- a/38-03.html +++ b/38-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 38.3 L38-3.C

  /* Sample program to exercise the polygon-filling routines. This code
@@ -163,16 +166,20 @@
 
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/38-04.html b/38-04.html index 13d5d80..91d5206 100644 --- a/38-04.html +++ b/38-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 38.2 isn’t particularly interesting; it merely draws each horizontal line in the passed-in list in the simplest possible way, one pixel at a time. (No, that doesn’t make the pixel the fundamental primitive; in the next chapter I’ll replace Listing 38.2 with a much faster version that doesn’t bother with individual pixels at all.)

Listing 38.1 is where the action is in this chapter. Our goal is to scan out the left and right edges of each polygon so that all points inside and no points outside the polygon are drawn, and so that all points located exactly on the boundary are drawn only if they are not on right or bottom edges. That’s precisely what Listing 38.1 does. Here’s how:

@@ -55,16 +58,20 @@

I’ve limited this chapter’s code to merely demonstrating the principles of filling convex polygons, and the listings given are by no means fast. In the next chapter, we’ll spice things up by eliminating the floating point calculations and pixel-at-a-time drawing and tossing a little assembly language into the mix.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/39-01.html b/39-01.html index 89b18b0..b678baa 100644 --- a/39-01.html +++ b/39-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 39
Fast Convex Polygons

@@ -69,22 +72,26 @@

The amount of time that the previous chapter’s sample program spent in each of these areas is shown in Table 39.1. As you can see, half the time was spent drawing and the other half was spent tracing the polygon edges (the time spent in FillConvexPolygon was relatively minuscule), so we have our choice of where to begin optimizing.

-

Fast Drawing

+

Fast Drawing

Let’s start with drawing, which is easily sped up. The previous chapter’s code used a double-nested loop that called a draw-pixel function to plot each pixel in the polygon individually. That’s a ridiculous approach in a graphics mode that offers linearly mapped memory, as does VGA mode 13H, the mode in which we’re working. At the very least, we could point a far pointer to the left edge of each polygon scan line, then draw each pixel in that scan line in quick succession, using something along the lines of *ScrPtr++ = FillColor; inside a loop.

However, it seems silly to use a loop when the x86 has an instruction, REP STOS, that’s uniquely suited to filling linear memory buffers. There’s no way to use REP STOS directly in C code, but it’s a good bet that the memset library function uses REP STOS, so you could greatly enhance performance by using memset to draw each scan line of the polygon in a single shot. That, however, is easier said than done. The memset function linked in from the library is tied to the memory model in use; in small (which includes Tiny, Small, or Medium) data models memset accepts only near pointers, so it can’t be used to access screen memory. Consequently, a large (which includes Compact, Large, or Huge) data model must be used to allow memset to draw to display memory—a clear case of the tail wagging the dog. This is an excellent example of why, although it is possible to use C to do virtually anything, it’s sometimes much simpler just to use a little assembly code and be done with it.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/39-02.html b/39-02.html index bc407d9..d26b9b5 100644 --- a/39-02.html +++ b/39-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

At any rate, Listing 39.1 for this chapter shows a version of DrawHorizontalLineList that uses memset to draw each scan line of the polygon in a single call. When linked to Chapter 38’s test program, Listing 39.1 increases pure drawing speed (disregarding edge tracing and other nondrawing time) by more than an order of magnitude over Chapter 38’s draw-pixel-based code, despite the fact that Listing 39.1 requires a large (in this case, the Compact) data model. Listing 39.1 works fine with Borland C++, but may not work with other compilers, for it relies on the aforementioned interaction between memset and the selected memory model.

@@ -241,7 +244,7 @@ void DrawHorizontalLineList(struct HLineList * HLineListPtr,

Anyway, Listing 39.1 has the desired effect of vastly improving drawing time. There are cycles yet to be had in the drawing code, but as tracing polygon edges now takes 92 percent of the polygon filling time, it’s logical to optimize the tracing code next.

-

Fast Edge Tracing

+

Fast Edge Tracing

There’s no secret as to why last chapter’s ScanEdge was so slow: It used floating point calculations. One secret of fast graphics is using integer or fixed-point calculations, instead. (Sure, the floating point code would run faster if a math coprocessor were installed, but it would still be slower than the alternatives; besides, why require a math coprocessor when you don’t have to?) Both integer and fixed-point calculations are fast. In many cases, fixed-point is faster, but integer calculations have one tremendous virtue: They’re completely accurate. The tiny imprecision inherent in either fixed or floating-point calculations can result in occasional pixels being one position off from their proper location. This is no great tragedy, but after going to so much trouble to ensure that polygons don’t overlap at common edges, why not get it exactly right?

@@ -255,16 +258,20 @@ void DrawHorizontalLineList(struct HLineList * HLineListPtr,
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/39-03.html b/39-03.html index 6aef6de..6f5a5b4 100644 --- a/39-03.html +++ b/39-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 39.2 shows a C implementation of integer edge tracing. Vertical and diagonal lines, which are trivial to trace, are special-cased. Other lines are broken into two categories: Y-major (closer to vertical) and X-major (closer to horizontal). The handlers for the Y-major and X-major cases operate on the principle of similar triangles: The number of X pixels advanced per scan line is the same as the ratio of the X delta of the edge to the Y delta. Listing 39.2 is more complex than the original floating point implementation, but not painfully so. In return for that complexity, Listing 39.2 is more than 80 times faster at scanning edges—and, as just mentioned, it’s actually more accurate than the floating point code.

Ya gotta love that integer arithmetic.

@@ -159,16 +162,20 @@ void ScanEdge(int X1, int Y1, int X2, int Y2, int SetXStart,

Listing 39.3 is an assembly language version of DrawHorizontalLineList . In actual use, it proved to be about 36 percent faster than Listing 39.1; better than a poke in the eye with a sharp stick, but just barely. There’s more to these timing results than meets that eye, though. Display memory generally responds much more slowly than system memory, especially in 386 and 486 systems. That means that much of the time taken by Listing 39.3 is actually spent waiting for display memory accesses to complete, with the processor forced to idle by wait states. If, instead, Listing 39.3 drew to a local buffer in system memory or to a particularly fast VGA, the assembly implementation might well display a far more substantial advantage over the C code.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/39-04.html b/39-04.html index 66d17e5..be4b2f1 100644 --- a/39-04.html +++ b/39-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

And indeed it does. When the test program is modified to draw to a local buffer, both the C and assembly language versions get 0.29 seconds faster, that being a measure of the time taken by display memory wait states. With those wait states factored out, the assembly language version of DrawHorizontalLineList becomes almost three times as fast as the C code.

@@ -135,7 +138,7 @@ _DrawHorizontalLineList endp end -

Maximizing REP STOS

+

Maximizing REP STOS

Listing 39.3 doesn’t take the easy way out and use REP STOSB to fill each scan line; instead, it uses REP STOSW to fill as many pixel pairs as possible via word-sized accesses, using STOSB only to do odd bytes. Word accesses to odd addresses are always split by the processor into 2-byte accesses. Such word accesses take twice as long as word accesses to even addresses, so Listing 39.3 makes sure that all word accesses occur at even addresses, by performing a leading STOSB first if necessary.

@@ -151,16 +154,20 @@ _DrawHorizontalLineList endp

By the way, note that all the versions of ScanEdge and FillConvexPolygon that we’ve looked at are adapter-independent, and that the C code is also machine-independent; all adapter-specific code is isolated in DrawHorizontalLineList. This makes it easy to add support for other graphics systems, such as the 8514/A, the XGA, or, for that matter, a completely non-PC system.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/39-05.html b/39-05.html index 94cbced..8c32d6f 100644 --- a/39-05.html +++ b/39-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 39.4 L39-4.ASM

 ; Scan converts an edge from (X1,Y1) to (X2,Y2), not including the
@@ -200,16 +203,20 @@ _ScanEdge   endp
         end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/40-01.html b/40-01.html index c184de7..bd18a91 100644 --- a/40-01.html +++ b/40-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 40
Of Songs, Taxes, and the Simplicity of Complex Polygons

@@ -51,7 +54,7 @@

Before we dive into complex polygon filling, I’d like to point out that the code in this chapter, like all polygon filling code I’ve ever seen, requires that the caller describe the type of the polygon to be filled. Often, however, the caller doesn’t know what type of polygon it’s passing, or specifies complex for simplicity, because that will work for all polygons; in such a case, the polygon filler will use the slow complex-fill code even if the polygon is, in fact, a convex polygon. In Chapter 41, I’ll discuss one way to improve this situation.

-

Active Edges

+

Active Edges

The basic premise of filling a complex polygon is that for a given scan line, we determine all intersections between the polygon’s edges and that scan line and then fill the spans between the intersections, as shown in Figure 40.1. (Section 3.6 of Foley and van Dam’s Computer Graphics, Second Edition provides an overview of this and other aspects of polygon filling.) There are several rules that might be used to determine which spans are drawn and which aren’t; we’ll use the odd/even rule, which specifies that drawing turns on after odd-numbered intersections (first, third, and so on) and off after even-numbered intersections.

@@ -67,16 +70,20 @@

Maintaining the AET from one scan line to the next involves three steps: First, we must add to the AET any edges that start on the current scan line, making sure to keep the AET X-sorted for efficient odd/even scanning. Second, we must remove edges that end on the current scan line. Third, we must advance the X coordinates of active edges with the same sort of error term-based, Bresenham’s-like approach we used for convex polygons, again ensuring that the AET is X-sorted after advancing the edges.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/40-02.html b/40-02.html index cad5986..2f5435a 100644 --- a/40-02.html +++ b/40-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Advancing the X coordinates is easy. For each edge, we’ll store the current X coordinate and all required error term information, and we’ll use that to advance the edge one scan line at a time; then, we’ll resort the AET by X coordinate as needed. Removing edges as they end is also easy; we’ll just count down the length of each active edge on each scan line and remove an edge when its count reaches zero. Adding edges as their tops are encountered is a tad more complex. While there are a number of ways to do this, one particularly efficient approach is to start out by putting all the edges of the polygon, sorted by increasing Y coordinate, into a single list, called the global edge table (GET). Then, as each scan line is encountered, all edges at the start of the GET that begin on the current scan line are moved to the AET; because the GET is Y-sorted, there’s no need to search the entire GET. For still greater efficiency, edges in the GET that share common Y coordinates can be sorted by increasing X coordinate; this ensures that no more than one pass through the AET per scan line is ever needed when adding new edges from the GET in such a way as to keep the AET sorted in ascending X order.

What form should the GET and AET take? Linked lists of edge structures, as shown in Figure 40.3. With linked lists, all that’s required to move edges from the GET to the AET as they become active, sort the AET, and remove edges that have been fully drawn is the exchanging of a few pointers.

@@ -338,16 +341,20 @@ }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/40-03.html b/40-03.html index a6421c0..70fa410 100644 --- a/40-03.html +++ b/40-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Complex Polygon Filling: An Implementation

Listing 40.1 just shown presents a function, FillPolygon(), that fills polygons of all shapes. If CONVEX_FILL_LINKED is defined, the fast convex fill code from Chapter 39 is linked in and used to draw convex polygons. Otherwise, convex polygons are handled as if they were complex. Nonconvex polygons are also handled as complex, although this is not necessary, as discussed shortly.

@@ -195,16 +198,20 @@ -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/40-04.html b/40-04.html index 9f70b42..930c686 100644 --- a/40-04.html +++ b/40-04.html @@ -1,5 +1,4 @@ - + @@ -19,28 +18,32 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 40.4 illustrates several interesting aspects of polygon filling. The first and third polygons drawn illustrate the operation of the odd/even fill rule. The second polygon drawn illustrates how holes can be created in seemingly solid objects; an edge runs from the outside of the rectangle to the inside, the edges comprising the hole are defined, and then the same edge is used to move back to the outside; because the edges join seamlessly, the rectangle appears to form a solid boundary around the hole.

The set of V-shaped polygons drawn by Listing 40.4 demonstrate that polygons sharing common edges meet but do not overlap. This characteristic, which I discussed at length in Chapter 38, is not a trivial matter; it allows polygons to fit together without fear of overlapping or missed pixels. In general, Listing 40.1 guarantees that polygons are filled such that common boundaries and vertices are drawn once and only once. This has the side-effect for any individual polygon of not drawing pixels that lie exactly on the bottom or right boundaries or at vertices that terminate bottom or right boundaries.

By the way, I have not seen polygon boundary filling handled precisely this way elsewhere. The boundary filling approach in Foley and van Dam is similar, but seems to me to not draw all boundary and vertex pixels once and only once.

-

More on Active Edges

+

More on Active Edges

Edges of zero height—horizontal edges and edges defined by two vertices at the same location—never even make it into the GET in Listing 40.1. A polygon edge of zero height can never be an active edge, because it can never intersect a scan line; it can only run along the scan line, and the span it runs along is defined not by that edge but by the edges that connect to its endpoints.

-

Performance Considerations

+

Performance Considerations

How fast is Listing 40.1? When drawing triangles on a 20-MHz 386, it’s less than one-fifth the speed of the fast convex polygon fill code. However, most of that time is spent drawing individual pixels; when Listing 40.2 is replaced with the fast assembly line segment drawing code in Listing 40.5, performance improves by two and one-half times, to about half as fast as the fast convex fill code. Even after conversion to assembly in Listing 40.5, DrawHorizontalLineSeg still takes more than half of the total execution time, and the remaining time is spread out fairly evenly over the various subroutines in Listing 40.1. Consequently, there’s no single place in which it’s possible to greatly improve performance, and the maximum additional improvement that’s possible looks to be a good deal less than two times; for that reason, and because of space limitations, I’m not going to convert the rest of the code to assembly. However, when filling a polygon with a great many edges, and especially one with a great many active edges at one time, relatively more time would be spent traversing the linked lists. In such a case, conversion to assembly (which does a very good job with linked list processing) could pay off reasonably well.

@@ -100,16 +103,20 @@

The algorithm used to X-sort the AET is an interesting performance consideration. Listing 40.1 uses a bubble sort, usually a poor choice for performance. However, bubble sorts perform well when the data are already almost sorted, and because of the X coherence of edges from one scan line to the next, that’s generally the case with the AET. An insertion sort might be somewhat faster, depending on the state of the AET when any particular sort occurs, but a bubble sort will generally do just fine.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/40-05.html b/40-05.html index a0cdd7f..e0c68fd 100644 --- a/40-05.html +++ b/40-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

An insertion sort that scans backward through the AET from the current edge rather than forward from the start of the AET could be quite a bit faster, because edges rarely move more than one or two positions through the AET. However, scanning backward requires a doubly linked list, rather than the singly linked list used in Listing 40.1. I’ve chosen to use a singly linked list partly to minimize memory requirements (double-linking requires an extra pointer field) and partly because supporting back links would complicate the code a good bit. The main reason, though, is that the potential rewards for the complications of back links and insertion sorting aren’t great enough; profiling a variety of polygons reveals that less than ten percent of total time is spent sorting the AET.

@@ -44,7 +47,7 @@

Nonconvex polygons can be filled somewhat faster than complex polygons. Because edges never cross or switch positions with other edges once they’re in the AET, the AET for a nonconvex polygon needs to be sorted only when new edges are added. In order for this to work, though, edges must be added to the AET in strict left-to-right order. Complications arise when dealing with two edges that start at the same point, because slopes must be compared to determine which edge is leftmost. This is certainly doable, but because of space limitations and limited performance returns, I haven’t implemented this in Listing 40.1.

-

Details, Details

+

Details, Details

Every so often, a programming demon that I’d thought I’d forever laid to rest arises to haunt me once again. A minor example of this—an imp, if you will—is the use of “ = ” when I mean “ == ,” which I’ve done all too often in the past, and am sure I’ll do again. That’s minor deviltry, though, compared to the considerably greater evils of one of my personal scourges, of which I was recently reminded anew: too-close attention to detail. Not seeing the forest for the trees. Looking low when I should have looked high. Missing the big picture, if you catch my drift.

@@ -60,16 +63,20 @@

Thanks, Anton.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/41-01.html b/41-01.html index 3c94c96..fbf2083 100644 --- a/41-01.html +++ b/41-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 41
Those Way-Down Polygon Nomenclature Blues

@@ -105,16 +108,20 @@ int PolygonIsMonotoneVertical(struct PointListHeader * VertexList) } -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/41-02.html b/41-02.html index 519d7f7..6fdf8aa 100644 --- a/41-02.html +++ b/41-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listings 41.2 and 41.3 are variants of the fast convex polygon fill code from Chapter 39, modified to be able to handle all monotone-vertical polygons, including nonsimple ones; the edge-scanning code (Listing 39.4 from Chapter 39) remains the same, and so is not shown again here.


@@ -157,16 +160,20 @@ int FillMonotoneVerticalPolygon(struct PointListHeader * VertexList, } -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/41-03.html b/41-03.html index ba6596a..7b10e6b 100644 --- a/41-03.html +++ b/41-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 41.3 L41-3.ASM

 ; Draws all pixels in list of horizontal lines passed in, in mode 13h, VGA’s 
@@ -126,16 +129,20 @@ _DrawHorizontalLineList endp
 
   

Listing 41.4 is almost identical to Listing 40.1 from Chapter 40. I’ve modified Listing 40.1 to employ the vertical-monotone detection test we’ve been talking about and use the fast vertical-monotone drawing code whenever possible; that’s what Listing 41.4 is. Note well that Listing 40.5 from Chapter 40 is also required in order for this code to link. Listing 41.5 is an appropriately updated version of the POLYGON.H header file.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/41-04.html b/41-04.html index 6851fc7..532a946 100644 --- a/41-04.html +++ b/41-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 41.4 L41-4.C

 /* Color-fills an arbitrarily-shaped polygon described by VertexList.
@@ -363,16 +366,20 @@ struct RGB { unsigned char Red, Green, Blue, Spare; };
 
   

See what accurate terminology and effective communication can do?

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/42-01.html b/42-01.html index a4d73f3..8518bd3 100644 --- a/42-01.html +++ b/42-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 42
Wu’ed in Haste; Fried, Stewed at Leisure

@@ -64,16 +67,20 @@

You might expect that the implementation of Wu antialiasing would fall into two distinct areas: tracing out the line (that is, finding the appropriate pixel pairs to draw) and calculating the appropriate weightings for each pixel pair. Not so, however. The weighting calculations involve only a few shifts, XORs, and adds; for all practical purposes, tracing and weighting are rolled into one step—and a very fast step it is. How fast is it? On a 33-MHz 486 with a fast VGA, a good but not maxed-out assembly implementation of Wu antialiasing draws a more than respectable 5,000 150-pixel-long vectors per second. That’s especially impressive considering that about 1,500,000 actual pixels are drawn per second, meaning that Wu antialiasing is drawing at around 50 percent of the maximum memory bandwidth—half the fastest theoretically possible drawing speed—of an AT-bus VGA. In short, Wu antialiasing is about as fast an antialiased line approach as you could ever hope to find for the VGA.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/42-02.html b/42-02.html index d2387e8..b0b916f 100644 --- a/42-02.html +++ b/42-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Tracing and Intensity in One

Horizontal, vertical, and diagonal lines do not require Wu antialiasing because they pass through the center of every pixel they meet; such lines can be drawn with fast, special-case code. For all other cases, Wu lines are traced out one step at a time along the major axis by means of a simple, fixed-point algorithm. The move along the minor axis with respect to a one-pixel move along the major axis (the line slope for lines with slopes less than 1, 1/slope for lines with slopes greater than 1) is calculated with a single integer divide. This value, called the “error adjust,” is stored as a fixed-point fraction, in 0.16 format (that is, all bits are fractional, and the decimal point is just to the left of bit 15). An error accumulator, also in 0.16 format, is initialized to 0. Then the first pixel is drawn; no weighting is needed, because the line intersects its endpoints exactly.

@@ -181,16 +184,20 @@ void DrawWuLine(int X0, int Y0, int X1, int Y1, int BaseColor, int NumLevels, }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/42-03.html b/42-03.html index a0bd485..2766f89 100644 --- a/42-03.html +++ b/42-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Sample Wu Antialiasing

The true test of any antialiasing technique is how good it looks, so let’s have a look at Wu antialiasing in action. Listing 42.1 is a C implementation of Wu antialiasing. Listing 42.2 is a sample program that draws a variety of Wu-antialiased lines, followed by non-antialiased lines, for comparison. Listing 42.3 contains DrawPixel() and SetMode() functions for mode 13H, the VGA’s 320x200 256-color mode. Finally, Listing 42.4 is a simple, non-antialiased line-drawing routine. Link these four listings together and run the resulting program to see both Wu-antialiased and non-antialiased lines.

@@ -196,16 +199,20 @@ void SetMode() }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/42-04.html b/42-04.html index e4a96cf..fd9a39e 100644 --- a/42-04.html +++ b/42-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 42.4 L42-4.C

 /* Function to draw a non-antialiased line from (X0,Y0) to (X1,Y1), using a
@@ -150,16 +153,20 @@ void SetMode()
 
   

Listing 42.1 isn’t very fast, so I implemented Wu antialiasing in assembly, hard-coded for mode 13H. The implementation is shown in full in Listing 42.6. High-speed graphics code and fast VGAs go together like peanut butter and jelly, which is to say very well indeed; the assembly implementation ran more than twice as fast as the C code on my 486. Enough said!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/42-05.html b/42-05.html index 8635d1e..2d37dc8 100644 --- a/42-05.html +++ b/42-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 42.6 L42-6.ASM

 ; C near-callable function to draw an antialiased line from
@@ -303,7 +306,7 @@ _DrawWuLine endp
         end
 
-

Notes on Wu Antialiasing

+

Notes on Wu Antialiasing

Wu antialiasing can be applied to any curve for which it’s possible to calculate at each step the positions and intensities of two bracketing pixels, although the implementation will generally be nowhere near as efficient as it is for lines. However, Wu’s article in Computer Graphics does describe an efficient algorithm for drawing antialiased circles. Wu also describes a technique for antialiasing solids, such as filled circles and polygons. Wu’s approach biases the edges of filled objects outward. Although this is no good for adjacent polygons of the sort used in rendering, it’s certainly possible to design a more accurate polygon-antialiasing approach around Wu’s basic weighting technique. The results would not be quite so good as more sophisticated antialiasing techniques, but they would be much faster.

@@ -323,16 +326,20 @@ _DrawWuLine endp

With or without symmetrical processing, Wu antialiasing beats fried, stewed chicken hands-down. Trust me on this one.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/43-01.html b/43-01.html index ad9e71a..aaf23a8 100644 --- a/43-01.html +++ b/43-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 43
Bit-Plane Animation

@@ -70,16 +73,20 @@


Figure 43.3
  The problem of overlapping colors.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/43-02.html b/43-02.html index b0ecb22..c58394b 100644 --- a/43-02.html +++ b/43-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Stacking the Palette Registers

+

Stacking the Palette Registers

Suppose that instead of viewing the four bits per pixel coming out of display memory as selecting one of sixteen colors,we view those bits as selecting one of four colors. If the bit from plane 0 is 1, that would select color 0 (say, red). The bit from plane 1 would select color 1 (say, green), the bit from plane 2 would select color 2 (say, blue), and the bit from plane 3 would select color 3 (say, white). Whenever more than 1 bit is 1, the 1 bit from the lowest-numbered plane would determine the color, and 1 bits from all other planes would be ignored. Finally, the absence of any 1 bits at all would select the background color (say, black).

@@ -216,16 +219,20 @@

Without further ado, Listing 43.1 shows bit-plane animation in action. Listing 43.1 animates 13 rather large images (each 32 pixels on a side) over a complex background at a good clip even on a primordial 8088-based PC. Five of the images move very quickly, while the other 8 bounce back and forth at a steady pace.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/43-03.html b/43-03.html index c282a7f..96d00e2 100644 --- a/43-03.html +++ b/43-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 43.1 L43-1.ASM

 ; Program to demonstrate bit-plane animation. Performs
@@ -529,16 +532,20 @@ Code    ends
         end     Start
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/43-04.html b/43-04.html index f12d292..bcc2d84 100644 --- a/43-04.html +++ b/43-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

For those of you who haven’t experienced the frustrations of animation programming on a PC, there’s a whole lot of animation going on in Listing 43.1. What’s more, the animation is virtually flicker-free, partly thanks to bit-plane animation and partly because images are never really erased but rather are simply overwritten. (The principle behind the animation is that of redrawing each image with a blank fringe around it when it moves, so that the blank fringe erases the part of the old image that the new image doesn’t overwrite. For details on this sort of animation, see the above-mentioned PC Tech Journal July 1986 article.) Better yet, the red images take precedence over the green images, which take precedence over the blue images, which take precedence over the white backdrop, and all obscured images show through holes in and around the edges of images in front of them.

In short, Listing 43.1 accomplishes everything we wished for earlier in an animation technique.

@@ -52,16 +55,20 @@

As you can see, the color yellow is displayed whenever a pixel’s bit from plane 3 is 1. This gives the images from plane 3 precedence, while leaving us with the 8 normal low-intensity colors for images drawn across the other 3 planes, as shown in Figure 43.5. Of course, this approach provides only 1 rather than 3 high-precedence planes, but that might be a good tradeoff for being able to draw multi-colored images as a backdrop to the high-precedence images. For the right application, high-speed flicker-free plane 3 images moving in front of an 8-color backdrop could be a potent combination indeed.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/43-05.html b/43-05.html index bcce8b4..b76a919 100644 --- a/43-05.html +++ b/43-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Another limitation of bit-plane animation is that it’s best if images stored in the same plane never cross each other. Why? Because when images do cross, the blank fringe

@@ -177,16 +180,20 @@

As Listing 43.1 runs, you may occasionally see an image shear, with the top and bottom parts of the image briefly offset. This is a consequence of drawing an image directly into memory as that memory is being scanned for video data. Occasionally the CRT controller scans a given area of display memory for pixel data just as the program is changing that same memory. If the CRT controller scans memory faster than the CPU can modify that memory, then the CRT controller can scan out the bytes of display memory that have been already been changed, pass the point in the image that the CPU is currently drawing, and start scanning out bytes that haven’t yet been changed. The result: Mismatched upper and lower portions of the image.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/43-06.html b/43-06.html index dc912c0..8fda09c 100644 --- a/43-06.html +++ b/43-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

If the CRT controller scans more slowly than the CPU can modify memory (likely with a 386, a fast VGA, and narrow images), then the CPU can rip right past the CRT controller, with the same net result of mismatched top and bottom parts of the image, as the CRT controller scans out first unchanged bytes and then changed bytes. Basically, shear will occasionally occur unless the CPU and CRT proceed at exactly the same rate, which is most unlikely. Shear is more noticeable when there are fewer but larger images, since it’s more apparent when a larger screen area is sheared, and because it’s easier to spot one out of three large images momentarily shearing than one out of twenty small images.

Image shear isn’t terrible—I’ve written and sold several games in which images occasionally shear, and I’ve never heard anyone complain—but neither is it ideal. One solution is page flipping, in which drawing is done to a non-displayed page of display memory while another page of display memory is shown on the screen. (We saw page flipping back in Chapter 23, we’ll see it again in the next chapter, and we’ll use it heavily starting in Chapter 47.) When the drawing is finished, the newly-drawn part of display memory is made the displayed page, so that the new screen becomes visible all at once, with no shearing or flicker. The other page is then drawn to, and when the drawing is complete the display is switched back to that page.

@@ -48,16 +51,20 @@

Bit-plane animation is neat stuff. Heck, good animation of any sort is fun, and the PC is as good a place as any (well, almost any) to make people’s jaws drop. (Certainly it’s the place to go if you want to make a lot of jaws drop.) Don’t let anyone tell you that you can’t do good animation on the PC. You can—if you stretch your mind to find ways to bring the full power of the VGA to bear on your applications. Bit-plane animation isn’t for every task; neither are page flipping, exclusive-ORing, pixel panning, or any of the many other animation techniques you have available. One or more tricks from that grab-bag should give you what you need, though, and the bigger your grab-bag, the better your programs.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/44-01.html b/44-01.html index c06d018..570723e 100644 --- a/44-01.html +++ b/44-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 44
Split Screens Save the Page Flipped Day

@@ -43,7 +46,7 @@

No horseshoes here.

-

A Plethora of Challenges

+

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.

@@ -51,20 +54,24 @@

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

+

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.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/44-02.html b/44-02.html index 3463b35..3e74d30 100644 --- a/44-02.html +++ b/44-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 44.1 L44-1.C

 /* Split screen VGA animation program. Performs page flipping in the
@@ -321,16 +324,20 @@ void MoveBouncer(bouncer *Bouncer, bumper *BumperPtr, int NumBumpers) {
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/44-03.html b/44-03.html index b5419aa..92b1c5b 100644 --- a/44-03.html +++ b/44-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 44.2 L44-2.ASM

 ; Low-level animation routines.
@@ -356,16 +359,20 @@ CharUpLoop:
         end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/44-04.html b/44-04.html index 57f355c..b9968e5 100644 --- a/44-04.html +++ b/44-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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

@@ -85,16 +88,20 @@ mov byte ptr es:[di],0ffh

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.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/44-05.html b/44-05.html index 635dbce..09e3c8a 100644 --- a/44-05.html +++ b/44-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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.

@@ -53,16 +56,20 @@


Figure 44.1
  Memory allocation for mode 10h page flipping.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/44-06.html b/44-06.html index fd4b28e..ea8b86a 100644 --- a/44-06.html +++ b/44-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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.

@@ -42,7 +45,7 @@

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

+

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.

@@ -61,16 +64,20 @@

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.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/45-01.html b/45-01.html index 7483c51..731d219 100644 --- a/45-01.html +++ b/45-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 45
Dog Hair and Dirty Rectangles

@@ -59,16 +62,20 @@

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, OUTs 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.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/45-02.html b/45-02.html index 302a319..880760e 100644 --- a/45-02.html +++ b/45-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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.

@@ -170,22 +173,26 @@


Figure 45.2
  Dirty rectangle animation.

-

So Why Not Use Page Flipping?

+

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.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/45-03.html b/45-03.html index 9558a51..020e4e0 100644 --- a/45-03.html +++ b/45-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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

@@ -298,16 +301,20 @@ void EraseEntities() }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/45-04.html b/45-04.html index 9bd677a..bc7ec91 100644 --- a/45-04.html +++ b/45-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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.

@@ -87,16 +90,20 @@ void Set640x400() } -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/45-05.html b/45-05.html index 1d4db9e..74cea6e 100644 --- a/45-05.html +++ b/45-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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

@@ -129,16 +132,20 @@ void Set640x400() } -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/45-06.html b/45-06.html index 39841bb..dc4631a 100644 --- a/45-06.html +++ b/45-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

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

@@ -78,16 +81,20 @@

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....)

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/46-01.html b/46-01.html index 23c2d24..13eb6d8 100644 --- a/46-01.html +++ b/46-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 46
Who Was that Masked Image?

@@ -45,7 +48,7 @@

We’re strange thinking machines, but we’re the best ones yet invented, and it’s worth learning how to tap our full potential. And with that, it’s back to dirty-rectangle animation.

-

Dirty-Rectangle Animation, Continued

+

Dirty-Rectangle Animation, Continued

In the last chapter, Introduced the idea of dirty-rectangle animation. This technique is an alternative to page flipping that’s capable of producing animation of very high visual quality, without any help at all from video hardware, and without the need for any extra, nondisplayed video memory. This makes dirty-rectangle animation more widely usable than page flipping, because many adapters don’t support page flipping. Dirty-rectangle animation also tends to be simpler to implement than page flipping, because there’s only one bitmap to keep track of. A final advantage of dirty-rectangle animation is that it’s potentially somewhat faster than page flipping, because display-memory accesses can theoretically be reduced to exactly one access for each pixel that changes from one frame to the next.

@@ -53,16 +56,20 @@

Listing 46.2 implements the low-level drawing routines in assembly language, which boosts performance a good deal. For maximum performance, it would be worthwhile to convert more of Listing 46.1 into assembly, so a call isn’t required for each animated image, and overall performance could be improved by streamlining the C code, but Listing 46.2 goes a long way toward boosting animation speed. This program now supports snappy animation of 15 images (as opposed to 10 for the software presented in the last chapter), and the images are now two pixels wider. That level of performance is all the more impressive considering that for this chapter I’ve converted the code from using rectangular images to using masked images.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/46-02.html b/46-02.html index fab5007..b0c0d58 100644 --- a/46-02.html +++ b/46-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 46.1 L46-1.C

 /* Sample simple dirty-rectangle animation program, partially optimized and
@@ -551,16 +554,20 @@ RowLoop3:
         end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/46-03.html b/46-03.html index b4e92f7..e25d0f8 100644 --- a/46-03.html +++ b/46-03.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Masked Images

+

Masked Images

Masked images are rendered by drawing an object’s pixels through a mask; pixels are actually drawn only where the mask specifies that drawing is allowed. This makes it possible to draw nonrectangular objects that don’t improperly interfere with one another when they overlap. Masked images also make it possible to have transparent areas (windows) within objects. Masked images produce far more realistic animation than do rectangular images, and therefore are more desirable. Unfortunately, masked images are also considerably slower to draw—however, a good assembly language implementation can go a long way toward making masked images draw rapidly enough, as illustrated by this chapter’s code. (Masked images are also known as sprites; some video hardware supports sprites directly, but on the PC it’s necessary to handle sprites in software.)

@@ -38,7 +41,7 @@

In this chapter, I’ve used the approach of having separate, paired masks and images. Another, quite different approach to masking is to specify a transparent color for copying, and copy only those pixels that are not the transparent color. This has the advantage of not requiring separate mask data, so it’s more compact, and the code to implement this is a little less complex than the full masking I’ve implemented. On the other hand, the transparent color approach is less flexible because it makes one color undrawable. Also, with a transparent color, it’s not possible to keep the same base image but use different masks, because the mask information is embedded in the image data.

-

Internal Animation

+

Internal Animation

I’ve added another feature essential to producing convincing animation: internal animation, which is the process of changing the appearance of a given object over time, as distinguished from changing only the location of a given object. Internal animation makes images look active and alive. I’ve implemented the simplest possible form of internal animation in Listing 46.1—alternation between two images—but even this level of internal animation greatly improves the feel of the overall animation. You could easily increase the number of images cycled through, simply by increasing the value of InternalAnimateMax for a given entity. You could also implement more complex image-selection logic to produce more interesting and less predictable internal-animation effects, such as jumping, ducking, running, and the like.

@@ -52,7 +55,7 @@

You might also try taking advantage of the natural coherence of animated graphics screens. In particular, because the rectangle used to erase an image at its old location often overlaps the rectangle within which the image resides at its new location, you could just directly generate the two or three nonoverlapped rectangles required to copy both the erase rectangle and the new-image rectangle for any single moving image. The calculation of these rectangles could be very efficient, given that you know in advance the direction of motion of your images. Handling this particular overlap case would eliminate most overlapped drawing, at a minimal cost. You might then decide to ignore overlapped drawing between different images, which tends to be both less common and more expensive to identify and handle.

-

Drawing Order and Visual Quality

+

Drawing Order and Visual Quality

A final note on dirty-rectangle animation concerns the quality of the displayed screen image. In the last chapter, we simply stuffed dirty rectangles into a list in the order they became dirty, and then copied all of the rectangles in that same order. Unfortunately, this caused all of the erase rectangles to be copied first, followed by all of the rectangles of the images at their new locations. Consequently, there was a significant delay between the appearance of the erase rectangle for a given image and the appearance of the new rectangle. A byproduct was the fact that a partially complete—part old, part new—image was visible long enough to be noticed. In short, although the pixels ended up correct, they were in an intermediate, incorrect state for a sufficient period of time to make the animation look wrong.

@@ -60,16 +63,20 @@

Avoid the trap of thinking animation is merely a matter of drawing the right pixels, one after another. Animation is the art of drawing the right pixels at the right times so that the eye and brain see what you want them to see. Animation is a lot more challenging than merely cranking out pixels, and it sure as heck isn’t a purely linear process.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/47-01.html b/47-01.html index f6321b3..0dfd6ff 100644 --- a/47-01.html +++ b/47-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 47
Mode X: 256-Color VGA Magic

@@ -65,16 +68,20 @@

Although some developers have taken advantage of Mode X, its use is certainly not universal, being entirely undocumented; only an experienced VGA programmer would have the slightest inkling that it even exists, and figuring out how to make it perform beyond the write pixel/read pixel level is no mean feat. Little other than my DDJ columns has been published about it, although John Bridges has widely distributed his code for a number of undocumented 256-color resolutions, and I’d like to acknowledge the influence of his code on the mode set routine presented in this chapter.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/47-02.html b/47-02.html index 459cc1d..14314c5 100644 --- a/47-02.html +++ b/47-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Given the tremendous advantages of Mode X over the documented mode 13H, I’d very much like to get it into the hands of as many developers as possible, so I’m going to spend the next few chapters exploring this odd but worthy mode. I’ll provide mode set code, delineate the bitmap organization, and show how the basic write pixel and read pixel operations work. Then, I’ll move on to the magic stuff: rectangle fills, screen clears, scrolls, image copies, pixel inversion, and, yes, polygon fills (just a different driver for the polygon code), all blurry fast; hardware raster ops; and page flipping. In the end, I’ll build a working animation program that shows many of the features of Mode X in action.

The mode set code is the logical place to begin.

@@ -142,16 +145,20 @@ _Set320x240Mode endp end -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/47-03.html b/47-03.html index 4f166af..f7332b5 100644 --- a/47-03.html +++ b/47-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

After setting up mode 13H, Listing 47.1 alters the vertical counts and timings to select 480 visible scan lines. (There’s no need to alter any horizontal values, because mode 13H and Mode X both have 320-pixel horizontal resolutions.) The Maximum Scan Line register is programmed to double scan each line (that is, repeat each scan line twice), however, so we get an effective vertical resolution of 240 scan lines. It is, in fact, possible to get 400 or 480 independent scan lines in 256-color mode, as discussed in Chapter 31 and 32; however, 400-scan-line modes lack square pixels and can’t support simultaneous off-screen memory and page flipping. Furthermore, 480-scan-line modes lack page flipping altogether, due to memory constraints.

At the same time, Listing 47.1 programs the VGA’s bitmap to a planar organization that is similar to that used by the 16-color modes, and utterly different from the linear bitmap of mode 13H. The bizarre bitmap organization of Mode X is shown in Figure 47.1. The first pixel (the pixel at the upper left corner of the screen) is controlled by the byte at offset 0 in plane 0. (The one thing that Mode X blessedly has in common with mode 13H is that each pixel is controlled by a single byte, eliminating the need to mask out individual bits of display memory.) The second pixel, immediately to the right of the first pixel, is controlled by the byte at offset 0 in plane 1. The third pixel comes from offset 0 in plane 2, and the fourth pixel from offset 0 in plane 3. Then, the fifth pixel is controlled by the byte at offset 1 in plane 0, and that cycle continues, with each group of four pixels spread across the four planes at the same address. The offset M of pixel N in display memory is M = N/4, and the plane P of pixel N is P = N mod 4. For display memory writes, the plane is selected by setting bit P of the Map Mask register (Sequence Controller register 2) to 1 and all other bits to 0; for display memory reads, the plane is selected by setting the Read Map register (Graphics Controller register 4) to P.

@@ -150,16 +153,20 @@ _ReadPixelX endp end -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/47-04.html b/47-04.html index 284aaec..c808c43 100644 --- a/47-04.html +++ b/47-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Designing from a Mode X Perspective

Listing 47.4 shows Mode X rectangle fill code. The plane is selected for each pixel in turn, with drawing cycling from plane 0 to plane 3, then wrapping back to plane 0. This is the sort of code that stems from a write-pixel line of thinking; it reflects not a whit of the unique perspective that Mode X demands, and although it looks reasonably efficient, it is in fact some of the slowest graphics code you will ever see. I’ve provided Listing 47.4 partly for illustrative purposes, but mostly so we’ll have a point of reference for the substantial speed-up that’s possible with code that’s designed from a Mode X perspective.

@@ -132,16 +135,20 @@ _FillRectangleX endp

Listing 47.5 is 2.5 times faster than Listing 47.4 at clearing the screen on a 20-MHz cached 386 with a Paradise VGA. Although Listing 47.5 is slightly slower than an equivalent mode 13H fill routine would be, it’s not grievously so.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/47-05.html b/47-05.html index 2f91c65..617b5c5 100644 --- a/47-05.html +++ b/47-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- @@ -169,16 +172,20 @@ _FillRectangleX endp end -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/47-06.html b/47-06.html index 9ff8605..345bfa9 100644 --- a/47-06.html +++ b/47-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Hardware Assist from an Unexpected Quarter

Listing 47.5 illustrates the benefits of designing code from a Mode X perspective; this is the software aspect of Mode X optimization, which suffices to make Mode X about as fast as mode 13H. That alone makes Mode X an attractive mode, given its square pixels, page flipping, and offscreen memory, but superior performance would nonetheless be a pleasant addition to that list. Superior performance is indeed possible in Mode X, although, oddly enough, it comes courtesy of the VGA’s hardware, which was never designed to be used in 256-color modes.

@@ -47,16 +50,20 @@

Note that the return from Mode X’s parallelism is not always 4x; some adapters lack the underlying memory bandwidth to write data that fast. However, Mode X parallel access should always be faster than mode 13H access; the only question on any given adapter is how much faster.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/47-07.html b/47-07.html index 9ac7cc0..bd09606 100644 --- a/47-07.html +++ b/47-07.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 47.6 L47-6.ASM

 ; Mode X (320x240, 256 colors) rectangle fill routine. Works on all
@@ -179,16 +182,20 @@ void main() {
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/48-01.html b/48-01.html index 351ef1f..8767c9f 100644 --- a/48-01.html +++ b/48-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 48
Mode X Marks the Latch

@@ -99,16 +102,20 @@ void main() { } -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/48-02.html b/48-02.html index c33e4ab..44dd967 100644 --- a/48-02.html +++ b/48-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 48.2 L48-2.ASM

 ; Mode X (320x240, 256 colors) rectangle 4x4 pattern fill routine.
@@ -209,16 +212,20 @@ _FillPatternX endp
         end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/48-03.html b/48-03.html index 2fbf4b0..6acbf20 100644 --- a/48-03.html +++ b/48-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Four-pixel-wide patterns are more useful than you might imagine. There are actually 2128 possible patterns (16 pixels, each with 28 possible colors); that set is certainly large enough for most color-dithering purposes, and includes many often-used patterns, such as halftones, diagonal stripes, and crosshatches.

Furthermore, eight-wide patterns, which are widely used, can be drawn with two passes, one for each half of the pattern. This principle can in fact be extended to patterns of arbitrary multiple-of-four widths. (Widths that aren’t multiples of four are considerably more difficult to handle, because the latches are four pixels wide; one possible solution is expanding such patterns via repetition until they are multiple-of-four widths.)

@@ -59,16 +62,20 @@ -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/48-04.html b/48-04.html index 046561e..bef9f41 100644 --- a/48-04.html +++ b/48-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 48.3 L48-3.ASM

 ; Mode X (320x240, 256 colors) display memory to display memory copy
@@ -207,16 +210,20 @@ _CopyScreenToScreenX endp
         end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/48-05.html b/48-05.html index 662345e..3d0d3c2 100644 --- a/48-05.html +++ b/48-05.html @@ -1,5 +1,4 @@ - + @@ -19,22 +18,26 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 48.3 has an important limitation: It does not guarantee proper handling when the source and destination overlap, as in the case of a downward scroll, for example. Listing 48.3 performs top-to-bottom, left-to-right copying. Downward scrolls require bottom-to-top copying; likewise, rightward horizontal scrolls require right-to-left copying. As it happens, my intended use for Listing 48.3 is to copy images between off-screen memory and on-screen memory, and to save areas under pop-up menus and the like, so I don’t really need overlap handling—and I do really need to keep the complexity of this discussion down. However, you will surely want to add overlap handling if you plan to perform arbitrary scrolling and copying in display memory.

Now that we have a fast way to copy images around in display memory, we can draw icons and other images as much as four times faster than in mode 13H, depending on the speed of the VGA’s display memory. (In case you’re worried about the nibble-alignment limitation on fast copies, don’t be; I’ll address that fully in due time, but the secret is to store all four possible rotations in off-screen memory, then select the correct one for each copy.) However, before our fast display memory-to-display memory copy routine can do us any good, we must have a way to get pixel patterns from system memory into display memory, so that they can then be copied with the fast copy routine.

-

Copying to Display Memory

+

Copying to Display Memory

The final piece of the puzzle is the system memory to display-memory-copy-routine shown in Listing 48.4. This routine assumes that pixels are stored in system memory in exactly the order in which they will ultimately appear on the screen; that is, in the same linear order that mode 13H uses. It would be more efficient to store all the pixels for one plane first, then all the pixels for the next plane, and so on for all four planes, because many OUTs could be avoided, but that would make images rather hard to create. And, while it is true that the speed of drawing images is, in general, often a critical performance factor, the speed of copying images from system memory to display memory is not particularly critical in Mode X. Important images can be stored in off-screen memory and copied to the screen via the latches much faster than even the speediest system memory-to-display memory copy routine could manage.

@@ -170,16 +173,20 @@ _CopySystemToScreenX endp

If you catch my drift.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/49-01.html b/49-01.html index 0546842..3ec5383 100644 --- a/49-01.html +++ b/49-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 49
Mode X 256-Color Animation

@@ -185,16 +188,20 @@ _CopySystemToScreenMaskedX endp end -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/49-02.html b/49-02.html index 6db6dbf..f4168f3 100644 --- a/49-02.html +++ b/49-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Faster Masked Copying

+

Faster Masked Copying

In the previous chapter we saw how the VGA’s latches can be used to copy four pixels at a time from one area of display memory to another in Mode X. We’ve further seen that in Mode X the Map Mask register can be used to select which planes are copied. That’s all we need to know to be able to perform fast masked copies; we can store an image in off-screen display memory, and set the Map Mask to the appropriate mask value as up to four pixels at a time are copied.

@@ -203,16 +206,20 @@ _CopyScreenToScreenMaskedX endp -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/49-03.html b/49-03.html index d758f0b..b360dc9 100644 --- a/49-03.html +++ b/49-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

It would be handy to have a function that, given a base image and mask, generates the four image and mask alignments and fills in the MaskedImage structure. Listing 49.3, together with the include file in Listing 49.4 and the system memory-to-display memory block-copy routine in Listing 48.4 (in the previous chapter) does just that. It would be faster if Listing 49.3 were in assembly language, but there’s no reason to think that generating aligned images needs to be particularly fast; in such cases, I prefer to use C, for reasons of coding speed, fewer bugs, and maintainability.

LISTING 49.3 L49-3.C

@@ -123,7 +126,7 @@ typedef struct { } MaskedImage; -

Notes on Masked Copying

+

Notes on Masked Copying

Listings 49.1 and 49.2, like all Mode X code I’ve presented, perform no clipping, because clipping code would complicate the listings too much. While clipping can be implemented directly in the low-level Mode X routines (at the beginning of Listing 49.1, for instance), another, potentially simpler approach would be to perform clipping at a higher level, modifying the coordinates and dimensions passed to low-level routines such as Listings 49.1 and 49.2 as necessary to accomplish the desired clipping. It is for precisely this reason that the low-level Mode X routines support programmable start coordinates in the source images, rather than assuming (0,0); likewise for the distinction between the width of the image and the width of the area of the image to draw.

@@ -137,16 +140,20 @@ typedef struct { -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/49-04.html b/49-04.html index e9ef7be..5e6754e 100644 --- a/49-04.html +++ b/49-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Animation

Gosh. There’s just no way I can discuss high-level animation fundamentals in any detail here; I could spend an entire (and entirely separate) book on animation techniques alone. You might want to have a look at Chapters 43 through 46 before attacking the code in this chapter; that will have to do us for the present volume. (I will return to 3-D animation in the next chapter.)

@@ -283,16 +286,20 @@ void MoveObject(AnimatedObject * ObjectToMove) { -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/49-05.html b/49-05.html index 32ca7fd..49980d3 100644 --- a/49-05.html +++ b/49-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Here’s something worth noting: The animation is extremely smooth on a 20 MHz 386. It is somewhat more jerky on an 8 MHz 286, because only 30 frames a second can be processed. If animation looks jerky on your PC, try reducing the number of kites.

The kites draw perfectly into the background, with no interference or fringe, thanks to masked copying. In fact, the kites also cross with no interference (the last-drawn kite is always in front), although that’s not readily apparent because they all look the same anyway and are moving fast. Listing 49.5 isn’t inherently limited to kites; create your own images and initialize the object list to display a mix of those images and see the full power of Mode X animation.

@@ -92,16 +95,20 @@ _ShowPage endp

There’s much more we could do with animation in general and with Mode X in particular, but it’s time to move on to new challenges. In closing, I’d like to point out that all of the VGA’s hardware features, including the built-in AND, OR, and XOR functions, are available in Mode X, just as they are in the standard VGA modes. If you understand the VGA’s hardware in mode 12H, try applying that knowledge to Mode X; you might be surprised at what you find you can do.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/50-01.html b/50-01.html index f2d1b81..f883532 100644 --- a/50-01.html +++ b/50-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 50
Adding a Dimension

@@ -63,16 +66,20 @@

That’s really all there is to basic 3-D drawing: transformation from object space to world space to view space to the screen. Next, we’ll look at the mechanics of transformation.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/50-02.html b/50-02.html index 62a436c..2a7f085 100644 --- a/50-02.html +++ b/50-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

One note: I’ll use a purely right-handed convention for coordinate systems. Right-handed means that if you hold your right hand with your fingers curled and the thumb sticking out, the thumb points along the Z axis and the fingers point in the direction of rotation from the X axis to the Y axis, as shown in Figure 50.2. Rotations about an axis are counter-clockwise, as viewed looking down an axis toward the origin. The handedness of a coordinate system is just a convention, and left-handed would do equally well; however, right-handed is generally used for object and world space. Sometimes, the handedness is flipped for view space, so that increasing Z equals increasing distance from the viewer along the line of sight, but I have chosen not to do that here, to avoid confusion. Therefore, Z decreases as distance along the line of sight increases; a view space coordinate of (0,0,-1000) is directly ahead, twice as far away as a coordinate of (0,0,-500).


@@ -38,18 +41,18 @@


Figure 50.2
  A right-handed coordinate system.

-

Projection

+

Projection

Working backward from the final image, we want to take the vertices of a polygon, as transformed into view space, and project them to 2-D coordinates on the screen, which, for projection purposes, is assumed to be centered on and perpendicular to the Z axis in view space, at some distance from the screen. We’re after visual realism, so we’ll want to do a perspective projection, in order that farther objects look smaller than nearer objects, and so that the field of view will widen with distance. This is done by scaling the X and Y coordinates of each point proportionately to the Z distance of the point from the viewer, a simple matter of similar triangles, as shown in Figure 50.3. It doesn’t really matter how far down the Z axis the screen is assumed to be; what matters is the ratio of the distance of the screen from the viewpoint to the width of the screen. This ratio defines the rate of divergence of the viewing pyramid—the full field of view—and is used for performing all perspective projections. Once perspective projection has been performed, all that remains before calling the polygon filler is to convert the projected X and Y coordinates to integers, appropriately clipped and adjusted as necessary to center the origin on the screen or otherwise map the image into a window, if desired.

-

Translation

+

Translation

Translation means adding X, Y, and Z offsets to a coordinate to move it linearly through space. Translation is as simple as it seems; it requires nothing more than an addition for each axis. Translation is, for example, used to move objects from object space, in which the center of the object is typically the origin (0,0,0), into world space, where the object may be located anywhere.


Figure 50.3
  Perspective projection.

-

Rotation

+

Rotation

Rotation is the process of circularly moving coordinates around the origin. For our present purposes, it’s necessary only to rotate objects about their centers in object space, so as to turn them to the desired attitude before translating them into world space.

@@ -71,16 +74,20 @@

Happily (and not coincidentally), we put together a nice 2-D animation framework back in Chapters 47, 48, and 49, during our exploratory discussion of Mode X, so we don’t have much to worry about in terms of non-3-D details. Basically, we’ll use Mode X (320x240, 256 colors), and we’ll flip between two display pages, drawing to one while the other is displayed. One new 2-D element that we need is the ability to clip polygons; while we could avoid this for the moment by restricting the range of motion of the polygon so that it stays fully on the screen, certainly in the long run we’ll want to be able to handle partially or fully clipped polygons. Listing 50.1 is the low-level code for a Mode X polygon filler that supports clipping. (The high-level polygon fill code is mode independent, and is the same as that presented in Chapters 38, 39, and 40, as noted further on.) The clipping is implemented at the low level, by trimming the Y extent of the scan line list up front, then clipping the X coordinates of each scan line in turn. This is not a particularly fast approach to clipping—ideally, the polygon would be clipped before it was scanned into a line list, avoiding potentially wasted scanning and eliminating the line-by-line X clipping—but it’s much simpler, and, as we shall see, polygon filling performance is the least of our worries at the moment.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/50-03.html b/50-03.html index e875c15..d4f535a 100644 --- a/50-03.html +++ b/50-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 50.1 L50-1.ASM

 ; Draws all pixels in the list of horizontal lines passed in, in
@@ -190,16 +193,20 @@ _DrawHorizontalLineList endp
         end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/50-04.html b/50-04.html index 2a805fc..b50c000 100644 --- a/50-04.html +++ b/50-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The other 2-D element we need is some way to erase the polygon at its old location before it’s moved and redrawn. We’ll do that by remembering the bounding rectangle of the polygon each time it’s drawn, then erasing by clearing that area with a rectangle fill.

With the 2-D side of the picture well under control, we’re ready to concentrate on the good stuff. Listings 50.2 through 50.5 are the sample 3-D animation program. Listing 50.2 provides matrix multiplication functions in a straightforward fashion. Listing 50.3 transforms, projects, and draws polygons. Listing 50.4 is the general header file for the program, and Listing 50.5 is the main animation program.

@@ -141,16 +144,20 @@ void XformAndProjectPoly(double Xform[4][4], struct Point3 * Poly, } -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/50-05.html b/50-05.html index b00c9df..8858c49 100644 --- a/50-05.html +++ b/50-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 50.4 POLYGON.H

 /* POLYGON.H: Header file for polygon-filling code, also includes
@@ -106,16 +109,20 @@ extern int DisplayedPage, NonDisplayedPage;
 extern struct Rect EraseRect[];
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/50-06.html b/50-06.html index f90f53c..c4e1c5a 100644 --- a/50-06.html +++ b/50-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 50.5 L50-5.C

 /* Simple 3-D drawing program to view a polygon as it rotates in
@@ -154,16 +157,20 @@ void main() {
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/50-07.html b/50-07.html index 49a6601..db5fca7 100644 --- a/50-07.html +++ b/50-07.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Notes on the 3-D Animation Example

+

Notes on the 3-D Animation Example

The sample program transforms the polygon’s vertices from object space to world space to view space to the screen, as described earlier. In this case, world space and view space are congruent—we’re looking right down the negative Z axis of world space—so the transformation matrix from world to view is the identity matrix; you might want to experiment with changing this matrix to change the viewpoint. The sample program uses 4x4 homogeneous coordinate matrices to perform transformations, as described above. Floating-point arithmetic is used for all 3-D calculations. Setting the translation from object space to world space is a simple matter of changing the appropriate entry in the fourth column of the object-to-world transformation matrix. Setting the rotation around the Y axis is almost as simple, requiring only the setting of the four matrix entries that control the Y rotation to the sines and cosines of the desired rotation. However, rotations involving more than one axis require multiple rotation matrices, one for each axis rotated around; those matrices are then concatenated together to produce the object-to-world transformation. This area is trickier than it might initially appear to be; more in the near future.

@@ -46,16 +49,20 @@

In the next chapter, we’ll assign fronts and backs to polygons, and start drawing only those that are facing the viewer. That will enable us to handle convex polyhedrons, such as tetrahedrons and cubes. We’ll also look at interactively controllable rotation, and at more complex rotations than the simple rotation around the Y axis that we did this time. In time, we’ll use fixed-point arithmetic to speed things up, and do some shading and texture mapping. The journey has only begun; we’ll get to all that and more soon.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/51-01.html b/51-01.html index 118597d..e8fae8c 100644 --- a/51-01.html +++ b/51-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 51
Sneakers in Space

@@ -57,16 +60,20 @@

One of the drawbacks of the previous chapter’s approach was that the polygon had two visible sides. Why is that a drawback? It isn’t, necessarily, but in our case we want to use polygons to build solid objects with continuous surfaces, and in that context, only one side of a polygon is visible; the other side always faces the inside of the object, and can never be seen. It would save time and simplify the process of hidden surface removal if we could quickly and easily determine whether the inside or outside face of each polygon was facing us, so that we could draw each polygon only if it were visible (that is, had the outside face pointing toward the viewer). On average, half the polygons in an object could be instantly rejected by a test of this sort. Such testing of polygon visibility goes by a number of names in the literature, including backplane culling, backface removal, and assorted variations thereon; I’ll refer to it as backface removal.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/51-02.html b/51-02.html index a443298..f8dfd34 100644 --- a/51-02.html +++ b/51-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

For a single convex polyhedron, removal of polygons that aren’t facing the viewer would solve all hidden surface problems. In a convex polyhedron, any polygon facing the viewer can never be obscured by any other polygon in that polyhedron; this falls out of the definition of a convex polyhedron. Likewise, any polygon facing away from the viewer can never be visible. Therefore, in order to draw a convex polyhedron, if you draw all polygons facing toward the viewer but none facing away from the viewer, everything will work out properly, with no additional checking for overlap and hidden surfaces needed.

Unfortunately, backface removal completely solves the hidden surface problem for convex polyhedrons only, and only if there’s a single convex polyhedron involved; when convex polyhedrons overlap, other methods must be used. Nonetheless, backface removal does instantly halve the number of polygons to be handled in rendering any particular scene. Backface removal can also speed hidden-surface handling if objects are built out of convex polyhedrons. In this chapter, though, we have only one convex polyhedron to deal with, so backface removal alone will do the trick.

@@ -52,20 +55,24 @@

Backface removal, as implemented in Listing 51.3, will not work reliably if the polygon is not convex, if the vertices don’t appear in clockwise order, if either the first or last edge in a polygon has zero length, or if the first and last edges are collinear. These latter two points are the reason it’s preferable to work in screen space rather than screen coordinates (which suffer from rounding problems), speed considerations aside.

-

Backface Removal in Action

+

Backface Removal in Action

Listings 51.1 through 51.5 together form a program that rotates a solid cube in real-time under user control. Listing 51.1 is the main program; Listing 51.2 performs transformation and projection; Listing 51.3 performs backface removal and draws visible faces; Listing 51.4 concatenates incremental rotations to the object-to-world transformation matrix; Listing 51.5 is the general header file. Also required from previous chapters are: Listings 50.1 and 50.2 from Chapter 50 (draw clipped line list, matrix math functions); Listings 47.1 and 47.6 from Chapter 47, (Mode X mode set, rectangle fill); Listing 49.6 from Chapter 49; Listing 39.4 from Chapter 39 (polygon edge scan); and the FillConvexPolygon() function from Listing 38.1 from Chapter 38. All necessary modules, along with a project file, will be present in the subdirectory for this chapter on the listings diskette, whether they were presented in this chapter or some earlier chapter. This may crowd the listings diskette a little bit, but it will certainly reduce confusion!

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/51-03.html b/51-03.html index b109f72..7569e26 100644 --- a/51-03.html +++ b/51-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 51.1 L51-1.C

 /* 3D animation program to view a cube as it rotates in Mode X. The viewpoint
@@ -194,16 +197,20 @@ void main() {
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/51-04.html b/51-04.html index 113185f..6b476c1 100644 --- a/51-04.html +++ b/51-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 51.2 L51-2.C

 /* Transforms all vertices in the specified object into view spa ce, then
@@ -138,16 +141,20 @@ void DrawVisibleFaces(struct Object * ObjectToXform)
   


Figure 51.4
  The object data structure

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/51-05.html b/51-05.html index 5c7c7f1..158f449 100644 --- a/51-05.html +++ b/51-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

With the above optimizations, the sample program is certainly adequately responsive on a 20 MHz 386 (sans 387; I’m sure it’s wonderfully responsive with a math coprocessor). Still, it couldn’t quite keep up with the keyboard when I modified it to read only one key each time through the loop—and we’re talking about only eight vertices here. This indicates that we’re already near the limit of animation complexity possible with our current approach. It’s time to start rethinking that approach; over two-thirds of the overall time is spent in floating-point calculations, and it’s there that we’ll begin to attack the performance bottleneck we find ourselves up against.

Incremental Transformation

@@ -103,16 +106,20 @@ }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/51-06.html b/51-06.html index 486f04b..87d2353 100644 --- a/51-06.html +++ b/51-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 51.5 POLYGON.H

 /* POLYGON.H: Header file for polygon-filling code, also includes a number of
@@ -126,16 +129,20 @@ extern struct Rect EraseRect[];
 
   

The screen space vertices are useful for some sorts of hidden surface removal. For example, to determine whether two polygons overlap as seen by the viewer, you must first know how they look to the viewer, accounting for perspective; screen space provides that information. (So do the final screen coordinates, but with less accuracy, and without any Z information.) The view space vertices are useful for collision and proximity detection; screen space can’t be used here, because objects are distorted by the perspective projection into screen space. World space would serve as well as view space for collision detection, but because it’s possible to transform directly from object space to view space with a single matrix, it’s often preferable to skip over world space. It’s not mandatory that vertices be stored for all these different spaces, but the coordinates in all those spaces have to be calculated as intermediate steps anyway, so we might as well keep them around for those occasions when they’re needed.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/52-01.html b/52-01.html index 4fcf3a5..e63f185 100644 --- a/52-01.html +++ b/52-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 52
Fast 3-D Animation: Meet X-Sharp

@@ -63,16 +66,20 @@

As always, all required files are in this chapter’s subdirectory on the CD-ROM.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/52-02.html b/52-02.html index ab01bcf..88d0da8 100644 --- a/52-02.html +++ b/52-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 52.1 L52-1.C

 /* 3-D animation program to rotate 12 cubes. Uses fixed point. All C code
@@ -155,16 +158,20 @@ void XformAndProjectPObject(PObject * ObjectToXform)
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/52-03.html b/52-03.html index 51df732..cff0c15 100644 --- a/52-03.html +++ b/52-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 52.3 L52-3.C

 /* Routines to perform incremental rotations around the three axes. */
@@ -191,16 +194,20 @@ void InitializeFixedPoint()
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/52-04.html b/52-04.html index e026161..d96c5f0 100644 --- a/52-04.html +++ b/52-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 52.6 L52-6.C

 /* Rotates and moves a polygon-based object around the three axes.
@@ -132,16 +135,20 @@ void DrawPObject(PObject * ObjectToXform)
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/52-05.html b/52-05.html index c8355da..741e62d 100644 --- a/52-05.html +++ b/52-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 52.8 L52-8.C

 /* Initializes the cubes and adds them to the object list. */
@@ -156,16 +159,20 @@ void InitializeCubes()
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/52-06.html b/52-06.html index 91fd706..5a3f0fb 100644 --- a/52-06.html +++ b/52-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 52.9 L52-9.ASM

 ; 386-specific fixed point multiply and divide.
@@ -209,16 +212,20 @@ extern Object *ObjectList[];
 extern Point3 CubeVerts[];
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/52-07.html b/52-07.html index 2fa652f..c280d5a 100644 --- a/52-07.html +++ b/52-07.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

A New Animation Framework: X-Sharp

Listings 52.1 through 52.10 shown earlier represent not merely faster animation in library form, but also a nearly complete, extensible, data-driven animation framework. Whereas much of the earlier animation code I’ve presented in this book was hardwired to demonstrate certain concepts, this chapter’s code is intended to serve as the basis for a solid animation package. Objects are stored, in their entirety, in customizable structures; new structures can be devised for new sorts of objects. Drawing, preparing for drawing, and moving are all vectored functions, so that variations such as shading or texturing, or even radically different sorts of graphics objects, such as scaled bitmaps, could be supported. The cube initialization is entirely data driven; more or different cubes, or other sorts of convex polyhedrons, could be added by simply changing the initialization data in Listing 52.8.

@@ -44,16 +47,20 @@

As of the previous chapter, we were at the point where we could rotate, move, and draw a solid cube in real time. Not too shabby...but the code I’m presenting in this chapter goes a bit further, rotating 12 solid cubes at an update rate of about 15 frames per second (fps) on a 20 MHz 386 with a slow VGA. That’s 12 transformation matrices, 72 polygons, and 96 vertices being handled in real time; not Star Wars, granted, but a giant step beyond a single cube. Run the program if you get a chance; you may be surprised at just how effective this level of animation is. I’d like to point out, in case anyone missed it, that this is fully general 3-D. I’m not using any shortcuts or tricks, like prestoring coordinates or pregenerating bitmaps; if you were to feed in different rotations or vertices, the animation would change accordingly.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/52-08.html b/52-08.html index a89e43f..eac6bd6 100644 --- a/52-08.html +++ b/52-08.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The keys to the performance increase manifested in this chapter’s code are three. The first key is fixed-point arithmetic. In the previous two chapters, we worked with floating-point coordinates and transformation matrices. Those values are now stored as 32-bit fixed-point numbers, in the form 16.16 (16 bits of whole number, 16 bits of fraction). 32-bit fixed-point numbers allow sufficient precision for 3-D animation, but can be manipulated with fast integer operations, rather than by slow floating-point processor operations or excruciatingly slow floating-point emulator operations. Although the speed advantage of fixed-point varies depending on the operation, on the processor, and on whether or not a coprocessor is present, fixed-point multiplication can be as much as 100 times faster than the emulated floating-point equivalent. (I’d like to take a moment to thank Chris Hecker for his invaluable input in this area.)

The second performance key is the use of the 386’s native 32-bit multiply and divide instructions. C compilers operating in real mode call library routines to perform multiplications and divisions involving 32-bit values, and those library functions are fairly slow, especially for division. On a 386, 32-bit multiplication and division can be handled with the bit of code in Listing 52.9—and most of even that code is only for rounding.

@@ -38,28 +41,32 @@

Just for fun, I reimplemented the animation of Listings 52.1 through 52.10 with floating-point instructions. Together, the preceeding optimizations improve the performance of the entire animation—including drawing time and overhead, and not just math—by more than ten times over the code that uses the floating-point emulator. Amazing what one can accomplish with a few dozen lines of assembly and a switch in number format, isn’t it? Note that no assembly code other than the native 386 multiply and divide is used in Listings 52.1 through 52.10, although the polygon fill code is of course mostly in assembly; we’ve achieved 12 cubes animated at 15 fps while doing the 3-D work almost entirely in Borland C++, and we’re still doing sine and cosine via the floating-point emulator. Happily, we’re still nowhere near the upper limit on the animation potential of the PC.

-

Drawbacks

+

Drawbacks

The techniques we’ve used to turbocharge 3-D animation are very powerful, but there’s a dark side to them as well. Obviously, native 386 instructions won’t work on 8088 and 286 machines. That’s rectifiable; equivalent multiplication and division routines could be implemented for real mode and performance would still be reasonable. It sure is nice to be able to plug in a 32-bit IMUL or DIV and be done with it, though. More importantly, 32-bit fixed-point arithmetic has limitations in range and accuracy. Points outside a 64Kx64Kx64K space can’t be handled, imprecision tends to creep in over the course of multiple matrix concatenations, and it’s quite possible to generate the dreaded divide by 0 interrupt if Z coordinates with absolute values less than one are used.

I don’t have space to discuss these issues in detail, but here are some brief thoughts: The working 64Kx64Kx64K fixed-point space can be paged into a larger virtual space. Imprecision of a pixel or two rarely matters in terms of display quality, and deterioration of concatenated rotations can be corrected by restoring orthogonality, for example by periodically calculating one row of the matrix as the cross-product of the other two (forcing it to be perpendicular to both). Alternatively, transformations can be calculated from scratch each time an object or the viewer moves, so there’s no chance for cumulative error. 3-D clipping with a front clip plane of -1 or less can prevent divide overflow.

-

Where the Time Goes

+

Where the Time Goes

The distribution of execution time in the animation code is no longer wildly biased toward transformation, but sine and cosine are certainly still sucking up cycles. Likewise, the overhead in the calls to FixedMul() and FixedDiv() is costly. Much of this is correctable with a little carefully crafted assembly language and a lookup table; I’ll provide that shortly.

Regardless, with this chapter we have made the critical jump to a usable level of performance and a serviceable general-purpose framework. From here on out, it’s the fun stuff.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/53-01.html b/53-01.html index ce52981..5cc2383 100644 --- a/53-01.html +++ b/53-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 53
Raw Speed and More

@@ -53,16 +56,20 @@

Listing 53.1 is the module FIXED.ASM from this chapter’s iteration of X-Sharp, with XformVec() and ConcatXforms() implemented in assembly language. The code is heavily optimized, to the extent of completely unrolling the loops via macros so that looping is eliminated altogether. FIXED.ASM is highly effective; the time taken for matrix math is now down to the point where it’s a fairly minor component of execution time, representing less than ten percent of the total. It’s time to turn our optimization sights elsewhere.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/53-02.html b/53-02.html index a14b743..634928b 100644 --- a/53-02.html +++ b/53-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 53.1 FIXED.ASM

 ; 386-specific fixed point routines.
@@ -421,16 +424,20 @@ ret
 end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/53-03.html b/53-03.html index b57b498..0ec05fe 100644 --- a/53-03.html +++ b/53-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Raw Speed, Part II: Look it Up

It’s a funny thing about Turbo Profiler: Time spent in the Borland C++ 80x87 emulator doesn’t show up directly anywhere that I can see in the timing results. The only way to detect it is by way of the line that reports what percent of total time is represented by all the areas that were profiled; if you’re profiling all areas, whatever’s not explicitly accounted for seems to be the floating-point emulator time. This quirk fooled me for a while, leading me to think sine and cosine weren’t major drags on performance, because the sin() and cos() functions spend most of their time in the emulator, and that time doesn’t show up in Turbo Profiler’s statistics on those functions. Once I figured out what was going on, it turned out that not only were sin() and cos() major drags, they were taking up over half the total execution time by themselves.

@@ -38,7 +41,7 @@

FIXED.ASM (Listing 53.1) speeds X-Sharp up quite a bit, and it changes the performance balance a great deal. When we started out with 3-D animation, calculation time was the dragon we faced; more than 90 percent of the total time was spent doing matrix and projection math. Additional optimizations in the area of math could still be made (using 32-bit multiplies in the backface-removal code, for example), but fixed-point math, the sine and cosine lookup, and selective assembly optimizations have done a pretty good job already. The bulk of the time taken by X-Sharp is now spent drawing polygons, drawing rectangles (to erase objects), and waiting for the page to flip. In other words, we’ve slain the dragon of 3-D math, or at least wounded it grievously; now we’re back to the dragon of polygon filling. We’ll address faster polygon filling soon, but for the moment, we have more than enough horsepower to have some fun with. First, though, we need one more feature: hidden surfaces.

-

Hidden Surfaces

+

Hidden Surfaces

So far, we’ve made a number of simplifying assumptions in order to get the animation to look good; for example, all objects must currently be convex polyhedrons. What’s more, right now, objects can never pass behind or in front of each other. What that means is that it’s time to have a look at hidden surfaces.

@@ -53,16 +56,20 @@


Figure 53.1
  Why back-to-front sorting doesn’t always work properly.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/53-04.html b/53-04.html index 5f3d5ec..73167e7 100644 --- a/53-04.html +++ b/53-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 53.2 OLIST.C

 /* Object list-related functions. */
@@ -115,28 +118,32 @@ void SortObjects()
 }
 
-

Rounding

+

Rounding

FIXED.ASM contains the equate ROUNDING-ON. When this equate is 1, the results of multiplications and divisions are rounded to the nearest fixed-point values; when it’s 0, the results are truncated. The difference between the results produced by the two approaches is, at most, 2-16; you wouldn’t think that would make much difference, now, would you? But it does. When the animation is run with rounding disabled, the cubes start to distort visibly after a few minutes, and after a few minutes more they look like they’ve been run over. In contrast, I’ve never seen any significant distortion with rounding on, even after a half-hour or so. I think the difference with rounding is not that it’s so much more accurate, but rather that the errors are evenly distributed; with truncation, the errors are biased, and biased errors become very visible when they’re applied to right-angle objects. Even with rounding, though, the errors will eventually creep in, and reorthogonalization will become necessary at some point.

The performance cost of rounding is small, and the benefits are highly visible. Still, truncation errors become significant only when they accumulate over time, as, for example, when rotation matrices are repeatedly concatenated over the course of many transformations. Some time could be saved by rounding only in such cases. For example, division is performed only in the course of projection, and the results do not accumulate over time, so it would be reasonable to disable rounding for division.

-

Having a Ball

+

Having a Ball

So far in our exploration of 3-D animation, we’ve had nothing to look at but triangles and cubes. It’s time for something a little more visually appealing, so the demonstration program now features a 72-sided ball. What’s particularly interesting about this ball is that it’s created by the GENBALL.C program in the BALL subdirectory of X-Sharp, and both the size of the ball and the number of bands of faces are programmable. GENBALL.C spits out to a file all the arrays of vertices and faces needed to create the ball, ready for inclusion in INITBALL.C. True, if you change the number of bands, you must change the Colors array in INITBALL.C to match, but that’s a tiny detail; by and large, the process of generating a ball-shaped object is now automated. In fact, we’re not limited to ball-shaped objects; substitute a different vertex and face generation program for GENBALL.C, and you can make whatever convex polyhedron you want; again, all you have to do is change the Colors array correspondingly. You can easily create multiple versions of the base object, too; INITCUBE.C is an example of this, creating 11 different cubes.

What we have here is the first glimmer of an object-editing system. GENBALL.C is the prototype for object definition, and INITBALL.C is the prototype for general-purpose object instantiation. Certainly, it would be nice to someday have an interactive 3-D object editing tool and resource management setup. We have our hands full with the drawing end of things at the moment, though, and for now it’s enough to be able to create objects in a semiautomated way.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/54-01.html b/54-01.html index 1b73f52..50873f2 100644 --- a/54-01.html +++ b/54-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 54
3-D Shading

@@ -49,16 +52,20 @@

At any rate, please keep in mind that the non-386 version of FixedDiv() is not a general-purpose 32x32 fixed-point division routine. In fact, it will generate a divide-by-zero error if passed a fixed-point divisor between -1 and 1. As I’ve explained, the non-386 version of Fixed-Div() is designed to do just what X-Sharp needs, and no more, as quickly as possible.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/54-02.html b/54-02.html index fbcec4e..daa4d6c 100644 --- a/54-02.html +++ b/54-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 54.1 FIXED.ASM

 ; Fixed point routines.
@@ -893,16 +896,20 @@ _ConcatXforms    endp
 end
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/54-03.html b/54-03.html index b1bf7d7..9e84915 100644 --- a/54-03.html +++ b/54-03.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Shading

+

Shading

So far, the polygons out of which our animated objects have been built have had colors of fixed intensities. For example, a face of a cube might be blue, or green, or white, but whatever color it is, that color never brightens or dims. Fixed colors are easy to implement, but they don’t make for very realistic animation. In the real world, the intensity of the color of a surface varies depending on how brightly it is illuminated. The ability to simulate the illumination of a surface, or shading, is the next feature we’ll add to X-Sharp.

@@ -56,16 +59,20 @@

The overall red shading for each polygon can be calculated by summing the ambient-shading red component with the diffuse-shading component from each light source, as in min((IAredxRred) + (IDred0xRredx(L0' • N)) + (IDred1xRredx(L1' • N)) +..., 1) where IDred0 and L0' are the red intensity and the reversed unit-direction vector, respectively, for spotlight 0. Listing 54.2 shows the X-Sharp module DRAWPOBJ.C, which performs ambient and diffuse shading. Toward the end, you will find the code that performs shading exactly as described by the above equation, first calculating the ambient red, green, and blue shadings, then summing that with the diffuse red, green, and blue shadings generated by each directed light source.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/54-04.html b/54-04.html index ce074ed..eee26d4 100644 --- a/54-04.html +++ b/54-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 54.2 DRAWPOBJ.C

 /* Draws all visible faces in the specified polygon-based object. The object
@@ -160,16 +163,20 @@ void DrawPObject(PObject * ObjectToXform)
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/54-05.html b/54-05.html index 6cc4683..d477cc4 100644 --- a/54-05.html +++ b/54-05.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Shading: Implementation Details

+

Shading: Implementation Details

In order to calculate the cosine of the angle between an incoming light source and a polygon’s unit normal, we must first have the polygon’s unit normal. This could be calculated by generating a cross-product on two polygon edges to generate a normal, then calculating the normal’s length and scaling to produce a unit normal. Unfortunately, that would require taking a square root, so it’s not a desirable course of action. Instead, I’ve made a change to X-Sharp’s polygon format. Now, the first vertex in a shaded polygon’s vertex list is the end-point of a unit normal that starts at the second point in the polygon’s vertex list, as shown in Figure 54.3. The first point isn’t one of the polygon’s vertices, but is used only to generate a unit normal. The second point, however, is a polygon vertex. Calculating the difference vector between the first and second points yields the polygon’s unit normal. Adding a unit-normal endpoint to each polygon isn’t free; each of those end-points has to be transformed, along with the rest of the vertices, and that takes time. Still, it’s faster than calculating a unit normal for each polygon from scratch.

@@ -44,16 +47,20 @@

Given the two unit vectors, it’s a piece of cake to calculate intensities, as shown in Listing 54.2. The sample program DEMO1, in the X-Sharp archive on the listings disk (built by running K1.BAT), puts the shading code to work displaying a rotating ball with ambient lighting and three spot lighting sources that the user can turn on and off. What you’ll see when you run DEMO1 is that the shading is very good—face colors change very smoothly indeed—so long as only green lighting sources are on. However, if you combine spotlight two, which is blue, with any other light source, polygon colors will start to shift abruptly and unevenly. As configured in the demo, the palette supports a wide range of shading intensities for a pure version of any one of the three primary colors, but a very limited number of intensity steps (four, in this case) for each color component when two or more primary colors are mixed. While this situation can be improved, it is fundamentally a result of the restricted capabilities of the 256-color palette, and there is only so much that can be done without a larger color set. In the next chapter, I’ll talk about some ways to improve the quality of 256-color shading.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/55-01.html b/55-01.html index fca42fe..accb5d3 100644 --- a/55-01.html +++ b/55-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 55
Color Modeling in 256-Color Mode

@@ -51,7 +54,7 @@

What does this anecdote tell us about the universe in which we live? Well, it certainly indicates that this universe is inhabited by at least one comedian and one good straight man. Beyond that, though, it can be construed as a parable about the difficulty of defining things properly; for example, consider the complications inherent in the definition of color on a 256-color display adapter such as the VGA. Coincidentally, VGA color modeling just happens to be this chapter’s topic, and the place to start is with color modeling in general.

-

A Color Model

+

A Color Model

We’ve been developing X-Sharp for several chapters now. In the previous chapter, we added illumination sources and shading; that addition makes it necessary for us to have a general-purpose color model, so that we can display the gradations of color intensity necessary to render illuminated surfaces properly. In other words, when a bright light is shining straight at a green surface, we need to be able to display bright green, and as that light dims or tilts to strike the surface at a shallower angle, we need to be able to display progressively dimmer shades of green.

@@ -84,16 +87,20 @@

When the JPL team went to test the eye’s sensitivity to color on the screen, they found that only about 16,000,000 colors could be distinguished, because the color-sensing mechanism of the human eye is more compatible with reflective sources such as paper and ink than with emissive sources such as CRTs. Still, the human eye can distinguish about 16,000,000 colors on the screen. That’s not so hard to believe, if you think about it; the eye senses each primary color separately, so we’re really only talking about detecting 256 levels of intensity per primary here. It’s the brain that does the amazing part; the 16,000,000-plus color capability actually comes not from extraordinary sensitivity in the eye, but rather from the brain’s ability to distinguish between all the mixes of 256 levels of each of three primaries.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/55-02.html b/55-02.html index 3cd254c..5b56522 100644 --- a/55-02.html +++ b/55-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

So it’s perfectly reasonable to maintain 24 bits of color resolution, and X-Sharp represents colors internally as ideal, device-independent 24-bit RGB triplets. All shading calculations are performed on these triplets, with 24-bit color precision. It’s only after the final 24-bit RGB drawing color is calculated that the display adapter’s color capabilities come into play, as the X-Sharp function ModelColorToColorIndex() is called to map the desired RGB color to the closest match the adapter is capable of displaying. Of course, that mapping is adapter-dependent. On a 24-bpp device, it’s pretty obvious how the internal RGB color format maps to displayed pixel colors: directly. On VGAs with 15-bpp Sierra Hicolor DACS, the mapping is equally simple, with the five upper bits of each color component mapping straight to display pixels. But how on earth do we map those 16,000,000-plus RGB colors into the 256-color space of a standard VGA?

This is the “color definition” problem I mentioned at the start of this chapter. The VGA palette is arbitrarily programmable to any set of 256 colors, with each color defined by six bits each of red, green, and blue intensity. In X-Sharp, the function InitializePalette() can be customized to set up the palette however we wish; this gives us nearly complete flexibility in defining the working color set. Even with infinite flexibility, however, 256 out of 16,000,000 or so possible colors is a pretty puny selection. It’s easy to set up the palette to give yourself a good selection of just blue intensities, or of just greens; but for general color modeling there’s simply not enough palette to go around.

@@ -125,16 +128,20 @@ }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/55-03.html b/55-03.html index 055544e..ad5b682 100644 --- a/55-03.html +++ b/55-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 55.3 L55-3.C

  /* Converts a model color (a color in the RGB color cube, in the current
@@ -67,7 +70,7 @@
 
   

The sad truth is that the VGA’s 256-color palette is an inadequate resource for general RGB shading. The good news is that clever workarounds can make VGA graphics look nearly as good as 24-bpp graphics; but the burden falls on you, the programmer, to design your applications and color mapping to compensate for the VGA’s limitations. To experiment with a different 256-color model in X-Sharp, just change InitializePalette() to set up the desired palette and ModelColorToColorIndex() to map 24-bit RGB triplets into the palette you’ve set up. It’s that simple, and the results can be striking indeed.

-

A Bonus from the BitMan

+

A Bonus from the BitMan

Finally, a note on fast VGA text, which came in from a correspondent who asked to be referred to simply as the BitMan. The BitMan passed along a nifty application of the VGA’s under-appreciated write mode 3 that is, under the proper circumstances, the fastest possible way to draw text in any 16-color VGA mode.

@@ -80,16 +83,20 @@

The keys to fast solid text are the latches and write mode 3. The latches, as you may recall from earlier discussions in this book, are four internal VGA registers that hold the last bytes read from the VGA’s four planes; every read from VGA memory loads the latches with the values stored at that display memory address across the four planes. Whenever a write is performed to VGA memory, the latches can provide some, none, or all of the bits written to memory, depending on the bit mask, which selects between the latched data and the drawing data on a bit-by-bit basis. The latches solve half our problem; we can fill the latches with the background color, then use them to draw the background box. The trick now is drawing the text pixels in the foreground color at the same time.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/55-04.html b/55-04.html index 005b6d1..99d4bab 100644 --- a/55-04.html +++ b/55-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

This is where it gets a little complicated. In write mode 3 (which incidentally is not available on the EGA), each byte value that the CPU writes to the VGA does not get written to display memory. Instead, it turns into the bit mask. (Actually, it’s ANDed with the Bit Mask register, and the result becomes the bit mask, but we’ll leave the Bit Mask register set to 0xFF, so the CPU value will become the bit mask.) The bit mask selects, on a bit-by-bit basis, between the data in the latches for each plane (the previously loaded background color, in this case) and the foreground color. Where does the foreground color come from, if not from the CPU? From the Set/Reset register, as shown in Figure 55.3. Thus, each byte written by the CPU (font data, presumably) selects foreground or background color for each of eight pixels, all done with a single write to display memory.


@@ -212,16 +215,20 @@ DrawTextString endp end start

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/56-01.html b/56-01.html index 53b0ef1..f993b82 100644 --- a/56-01.html +++ b/56-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 56
Pooh and the Space Station

@@ -54,7 +57,7 @@


Figure 56.1
  Using reverse transformation to find the source pixel color.

-

Mapping Textures Made Easy

+

Mapping Textures Made Easy

To understand how we’re going to map textures, consider Figure 56.2, which maps a bitmapped image directly onto an untransformed polygon. Here, we simply map the origin of the polygon’s untransformed coordinate system somewhere within the image, then map the vertices to the corresponding image pixels. (For simplicity, I’ll assume in this discussion that the polygon’s coordinate system is in units of pixels, but scaling images to polygons is eminently doable. This will become clearer when we look at mapping images onto transformed polygons, next.) Mapping the image to the polygon is then a simple matter of stepping one scan line at a time in both the image and the polygon, each time advancing the X coordinates of the edges according to the slopes of the lines, just as is normally done when filling a polygon. Since the polygon is untransformed, the stepping is identical in both the image and the polygon, and the pixel mapping is one-to-one, so the appropriate part of each scan line of the image can simply be block copied to the destination.

@@ -65,16 +68,20 @@

The solution is remarkably simple. We’ll just map each transformed vertex to the corresponding vertex in the bitmap; this is easy, because the vertices are at the same indices in the original and transformed vertex lists. Each time we select a new edge to scan for the destination polygon, we’ll select the corresponding edge in the source bitmap, as well. Then—and this is crucial—each time we step a destination edge one scan line, we’ll step the corresponding source image edge an equivalent amount.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/56-02.html b/56-02.html index d87f427..968e0bb 100644 --- a/56-02.html +++ b/56-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Ah, but what is an “equivalent amount”? Think of it this way. If a destination edge is 100 scan lines high, it will be stepped 100 times. Then, we’ll divide the SourceXWidth and SourceYHeight lengths of the source edge by 100, and add those amounts to the source edge’s coordinates each time the destination is stepped one scan line. Put another way, we have, as usual, arranged things so that in the destination polygon we step DestYHeight times, where DestYHeight is the height of the destination edge. The this approach arranges to step the source image edge DestYHeight times also, to match what the destination is doing.


@@ -45,7 +48,7 @@


Figure 56.5
  Mapping a texture onto a narrower polygon.

-

Notes on DDA Texture Mapping
+

Notes on DDA Texture Mapping

That’s all there is to quick-and-dirty texture mapping. This technique basically uses a two-stage digital differential analyzer (DDA) approach to step through the appropriate part of the source image in tandem with the normal scan-line stepping through the destination polygon, so I’ll call it “DDA texture mapping.” It’s worth noting that there is no need for any trigonometric functions at all, and only two divides are required per scan line.

@@ -58,16 +61,20 @@

For now, all we need is fast texture mapping of adequate quality, which the straightforward, non-antialiased DDA approach supplies. I’m sure there are many other fast approaches, and, as I’ve said, there are more accurate approaches, but DDA texture mapping works well, given the constraints of the PC’s horsepower. Next, we’ll look at code that performs DDA texture mapping. First, though, I’d like to take a moment to thank Jim Kent, author of Autodesk Animator and a frequent correspondent, for getting me started with the DDA approach.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/56-03.html b/56-03.html index 18f9eb9..49e1de4 100644 --- a/56-03.html +++ b/56-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Fast Texture Mapping: An Implementation

As you might expect, I’ve implemented DDA texture mapping in X-Sharp, and the changes are reflected in the X-Sharp archive in this chapter’s subdirectory on the listings disk. Listing 56.1 shows the new header file entries, and Listing 56.2 shows the actual texture-mapped polygon drawer. The set-pixel routine that Listing 56.2 calls is a slight modification of the Mode X set-pixel routine from Chapter 47. In addition, INITBALL.C has been modified to create three texture-mapped polygons and define the texture bitmaps, and modifications have been made to allow the user to flip the axis of rotation. You will of course need the complete X-Sharp library to see texture mapping in action, but Listings 56.1 and 56.2 are the actual texture mapping code in its entirety.

@@ -350,16 +353,20 @@ void ScanOutLine(EdgeScan * LeftEdge, EdgeScan * RightEdge)

And, in case you’re curious, yes, there is a bear in DEMO1. I wouldn’t say he looks much like a Pooh-type bear, but he’s a bear nonetheless. He does tend to look a little startled when you flip the ball around so that he’s zipping by on his head, but, heck, you would too in the same situation. And remember, when you buy the next VGA megahit, Bears in Space, you saw it here first.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/57-01.html b/57-01.html index e8861fa..9941610 100644 --- a/57-01.html +++ b/57-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 57
10,000 Freshly Sheared Sheep on the Screen

@@ -39,7 +42,7 @@

The chap doing the shearing did say one thing that stuck with me, although it may not sound particularly profound. (Actually, it sounds pretty silly, but bear with me.) He said, “You don’t get really good at sheep shearing for 10 years, or 10,000 sheep.” I’ll buy that. In fact, to extend that morsel of wisdom to the greater, non-ovine-centric universe, it actually takes a good chunk of experience before you get good at anything worthwhile—especially graphics, for a couple of reasons. First, performance matters a lot in graphics, and performance programming is largely a matter of experience. You can’t speed up PC graphics simply by looking in a book for a better algorithm; you have to understand the code C compilers generate, assembly language optimization, VGA hardware, and the performance implications of various graphics-programming approaches and algorithms. Second, computer graphics is a matter of illusion, of convincing the eye to see what you want it to see, and that’s very much a black art based on experience.

-

Visual Quality: A Black Hole ... Er, Art

+

Visual Quality: A Black Hole ... Er, Art

Pleasing the eye with realtime computer animation is something less than a science, at least at the PC level, where there’s a limited color palette and no time for antialiasing; in fact, sometimes it can be more than a little frustrating. As you may recall, in the previous chapter I implemented texture mapping in X-Sharp. There was plenty of experience involved there, some of which I didn’t mention. My first implementation was disappointing; the texture maps shimmied and sheared badly, like a loosely affiliated flock of pixels, each marching to its own drummer. Then, I added a control key to speed up the rotation; what a difference! The aliasing problems were still there, but with the faster rotation, the pixels moved too quickly for the eye to pick up on the aliasing; the rotating texture maps, and the rotating ball as a whole, crossed the threshold into being accepted by the eye as a viewed object, rather than simply a collection of pixels.

@@ -53,7 +56,7 @@ -

Fixed-Point Arithmetic, Redux

+

Fixed-Point Arithmetic, Redux

In the previous chapter I added texture mapping to X-Sharp, but lacked space to explain some of its finer points. I’ll pick up the thread now and cover some of those points here, and discuss the visual and performance enhancements that previous chapter’s code needed—and which are now present in the version of X-Sharp in this chapter’s subdirectory on the CD-ROM.

@@ -69,16 +72,20 @@

Experience again: It’s the difference between knowing which flaws (like small texture shifts) can reasonably be ignored, and which (like those that produce gaps between polygons) must be avoided at all costs.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/57-02.html b/57-02.html index 11a8d7c..02a8fbd 100644 --- a/57-02.html +++ b/57-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Texture Mapping: Orientation Independence

+

Texture Mapping: Orientation Independence

The double-DDA texture-mapping code presented in the previous chapter worked adequately, but there were two things about it that left me less than satisfied. One flaw was performance; I’ll address that shortly. The other flaw was the way textures shifted noticeably as the orientations of the polygons onto which they were mapped changed.

@@ -115,7 +118,7 @@ void ScanOutLine(EdgeScan * LeftEdge, EdgeScan * RightEdge) } -

Mapping Textures across Multiple Polygons

+

Mapping Textures across Multiple Polygons

One of the truly nifty things about double-DDA texture mapping is that it is not limited to mapping a texture onto a single polygon. A single texture can be mapped across any number of adjacent polygons simply by having polygons that share vertices in 3-space also share vertices in the texture map. In fact, the demonstration program DEMO1 in the X-Sharp archive maps a single texture across two polygons; this is the blue-on-green pattern that stretches across two panels of the spinning ball. This capability makes it easy to produce polygon-based objects with complex surfaces (such as banding and insignia on spaceships, or even human figures). Just map the desired texture onto the underlying polygonal framework of an object, and let double-DDA texture mapping do the rest.

@@ -127,16 +130,20 @@ void ScanOutLine(EdgeScan * LeftEdge, EdgeScan * RightEdge)

Listing 57.2 is a high-performance assembly language implementation of Listing 57.1. Apart from the conversion to assembly language, this implementation improves performance by focusing on reducing inner loop bottlenecks. In fact, the whole of Listing 57.2 is nothing more than the inner loop for texture-mapped polygon drawing; Listing 57.2 is only the code to draw a single scan line. Most of the work in drawing a texture-mapped polygon comes in scanning out individual lines, though, so this is the appropriate place to optimize.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/57-03.html b/57-03.html index 342c95b..06343db 100644 --- a/57-03.html +++ b/57-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 57.2 L57-2.ASM

 ; Draws all pixels in the specified scan line, with the pixel colors
@@ -333,16 +336,20 @@ ScanDone:
 
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/57-04.html b/57-04.html index 7a37ef8..1a721ae 100644 --- a/57-04.html +++ b/57-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Within Listing 57.2, all the important optimization is in the loop that draws across each destination scan line, near the end of the listing. One optimization is elimination of the call to the set-pixel routine used to draw each pixel in Listing 57.1. Function calls are expensive operations, to be avoided when performance matters. Also, although Mode X (the undocumented 320x240 256-color VGA mode X-Sharp runs in) doesn’t lend itself well to pixel-oriented operations like line drawing or texture mapping, the inner loop has been set up to minimize Mode X’s overhead. A rotating plane mask is maintained in AL, with DX pointing to the Map Mask register; thus, only a rotate and an OUT are required to select the plane to which to write, cycling from plane 0 through plane 3 and wrapping back to 0. Better yet, because we know that we’re simply stepping horizontally across the destination scan line, we can use a clever optimization to both step the destination and reduce the overhead of maintaining the mask. Two copies of the current plane mask are maintained, one in each nibble of AL. (The Map Mask register pays attention only to the lower nibble.) Then, when one copy rotates out of the lower nibble, the other copy rotates into the lower nibble and is ready to be used. This approach eliminates the need to test for the mask wrapping from plane 3 to plane 0, all the more so because a carry is generated when wrapping occurs, and that carry can be added to DI to advance the screen pointer. (Check out the next chapter, however, to see the best Map Mask optimization of all—setting it once and leaving it unchanged.)

In all, the overhead of drawing each pixel is reduced from a call to the set-pixel routine and full calculation of the screen address and plane mask to five instructions and no branches. This is an excellent example of converting full, from-scratch calculations to incremental processing, whereby only information that has changed since the last operation (the plane mask moving one pixel, for example) is recalculated.

@@ -40,16 +43,20 @@

I’m always interested in getting your feedback on and hearing about potential improvements to X-Sharp. Contact me through the publisher. There is no truth to the rumor that I can be reached under the alias “sheep-shearer,” at least not for another 9,999 sheep.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/58-01.html b/58-01.html index 93687a4..b4317c6 100644 --- a/58-01.html +++ b/58-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 58
Heinlein’s Crystal Ball, Spock’s Brain, and the 9-Cycle Dare

@@ -59,7 +62,7 @@

It was the “Hmph” that really got to me.

-

Left-Brain Optimization

+

Left-Brain Optimization

That was the first shot of juice for my optimizer (or at least blow to my ego, which can be just as productive). John went on to say he had gotten texture mapping down to 9 cycles per pixel and one jump per scanline on a 486 (all cycle times will be for the 486 unless otherwise noted); given that my code took, on average, about 44 cycles and 2 taken jumps (plus 1 not taken) per pixel, I had a long way to go.

@@ -68,16 +71,20 @@


Figure 58.1
  Texture mapping a single horizontal scanline.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/58-02.html b/58-02.html index 89e655d..e9cda06 100644 --- a/58-02.html +++ b/58-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 58.1 L58-1.ASM

 ; Inner loop to draw a single texture-mapped horizontal scanline in
@@ -94,7 +97,7 @@ NoExtraYAdvance:
 
   

Why indeed?

-

A 90-Degree Shift in Perspective

+

A 90-Degree Shift in Perspective

As I said earlier, how you look at an optimization problem defines how you’ll be able to solve it. In order to boost performance, sometimes it’s necessary to look at things from a different angle—and for texture mapping this was literally as well as figuratively true. Chris suggested nothing more nor less than scanning out polygons at a 90-degree angle to normal, starting, say, at the left edge of the polygon, and texture-mapping vertically along each column of pixels, as shown in Figure 58.3. That way, all the pixels in each texture-mapped column would be in the same plane, and I would need to change planes only between columns—outside the inner loop. A trivial change, not fundamental in any sense—and yet just that one change, plus unrolling the loop, reduced the inner loop to the 22-cycles-per-pixel version shown in Listing 58.2. That’s exactly twice as fast as Listing 58.1—and given how incredibly slow most VGAs are at completing OUTs, the real-world speedup should be considerably greater still. (The fastest byte OUT I’ve ever measured for a VGA is 29 cycles, the slowest more than 60 cycles; in the latter case, Listing 58.2 would be on the order of four times faster than Listing 58.1.)

@@ -144,16 +147,20 @@ ENDM
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/58-03.html b/58-03.html index 69405f5..c111b3a 100644 --- a/58-03.html +++ b/58-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

I’d like to emphasize that algorithmically and conceptually, there is no difference between scanning out a polygon top to bottom and scanning it out left to right; it is only in conjunction with the hardware organization of Mode X that the scanning direction matters in the least.

@@ -90,16 +93,20 @@ MOV [EDI+SCANOFFSET],AH

Actually, we also advanced the source pointer by the Y integer amount back when we added BP to SI; all that’s left is to detect whether our addition to the Y fractional current coordinate produced a carry. That’s easily done by testing bit 15 of EDX; if it’s zero, there was no carry and we’re done; otherwise, Y carried, so we have to reset bit 15 and advance the source pointer by one scanline. The resulting program flow is shown in Figure 58.7. Note that unlike the X fractional addition, we can’t get away with just adding in the carry from the Y fractional addition, because when the Y fraction carries, it indicates a move not from one pixel to the next on a scanline (a single byte), but rather from one scanline to the next (a full scanline width).

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/58-04.html b/58-04.html index bca3007..98a005c 100644 --- a/58-04.html +++ b/58-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

All of the above optimizations together get us to 10 cycles—very close to John Miles, but not there yet. We have one more trick up our sleeve, though: Suppose we point SS to the segment containing our textures, and point DS to the screen? (This requires either setting up a stack in the texture segment or ensuring that interrupts and other stack activity can’t happen while SS points to that segment.) Then, we could swap the functions of SI and BP; that would let us use BP, which accesses SS by default, to get at the textures, and DI to access the screen—all with no segment prefixes at all. By gosh, that would get us exactly one more cycle, and would bring us down to the same 9 cycles John Miles attained; Listing 58.3 shows that code. At long last, the Holy Grail attained and our honor defended, we can rest.

Or can we?

@@ -79,22 +82,26 @@ SCANOFFSET = SCANOFFSET + SCANWIDTH ENDM -

Don’t Stop Thinking about Those Cycles

+

Don’t Stop Thinking about Those Cycles

Remember what I said at the outset, that knowing something has been done makes it much easier to do? A corollary is that pushing past that point, once attained, is very difficult. It’s only natural to want to relax in the satisfaction of a job well done; then, too, the very nature of the work changes. Getting from 44 cycles down to John’s 9 cycles was a huge leap, but we knew it could be done—therefore the nature of the problem was to figure out how it was done; in cases like this, if we’re sharp enough (and of course we are!), we’re guaranteed eventual gratification. Now that we’ve reached John’s level of performance, the problem becomes whether the code can be made faster yet, and that’s a different kettle of fish altogether, for it may well be that after thinking about it for a while, we’ll conclude that it can’t. Not only will we have wasted time, but we’ll also never be sure we were right; we’ll know only that we couldn’t find a solution. That way lies madness.

And yet—someone has to blaze the trail to higher performance, and that someone might as well be us. Let’s look for weaknesses in Listing 58.3. None are readily apparent; the only cycle that looks even slightly wasted is the size prefix on ADD EDX,ECX. As it turns out, that cycle really is wasted, for there’s a way to make the size prefix vanish without losing the benefits of 32-bit instructions: Move the code into a 32-bit segment and make all the instructions 32-bit. That’s what Listing 58.4 does; this code is similar to Listing 58.3, but runs in 8 cycles per pixel, a 12.5 percent speedup over Listing 58.3. Whether Listing 58.4 actually draws more pixels per second than Listing 58.3 depends on whether display memory is fast enough to handle pixels as rapidly as Listing 58.4 can deliver them. That speed, one pixel every 122 nanoseconds on a 486/66, is one that ISA adapters can’t hope to match, but fast VLB and PCI adapters can handle with ease. Be aware, too, that cache misses when reading the source texture will generally reduce performance below the calculated 8-cycles-per-pixel level, especially because textures, which can be scanned across at any angle, are rarely accessed at consecutive addresses, which is the arrangement that would make for the fewest cache misses.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/58-05.html b/58-05.html index 0819e83..ffe15db 100644 --- a/58-05.html +++ b/58-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 58.4 L58-4.ASM

 ; Inner loop to draw a single texture-mapped vertical column,
@@ -96,16 +99,20 @@ ADD EDX,EBP     ;cycle 3 V-pipe
 
   

Believe it! And while you’re at it, give both halves of your brain equal time—and watch out for aliens in short skirts, 60’s bouffant hairdos, and an undue interest in either half.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/59-01.html b/59-01.html index 2799845..f0d3c7c 100644 --- a/59-01.html +++ b/59-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 59
The Idea of BSP Trees

@@ -67,20 +70,24 @@

Building a tree that subdivides space doesn’t sound particularly profound, but there’s a lot that can be done with such a structure. BSP trees can be used to represent shapes, and operating on those shapes is a simple matter of combining trees as needed; this makes BSP trees a powerful way to implement Constructive Solid Geometry (CSG). BSP trees can also be used for hit testing, line-of-sight determination, and collision detection.

-

Visibility Determination

+

Visibility Determination

For the time being, I’m going to discuss only one of the many uses of BSP trees: The ability of a BSP tree to allow you to traverse a set of line segments or polygons in back-to-front or front-to-back order as seen from any arbitrary viewpoint. This sort of traversal can be very helpful in determining which parts of each line segment or polygon are visible and which are occluded from the current viewpoint in a 3-D scene. Thus, a BSP tree makes possible an efficient implementation of the painter’s algorithm, whereby polygons are drawn in back-to-front order, with closer polygons overwriting more distant ones that overlap, as shown in Figure 59.1. (The line segments in Figure 1(a) and in other figures in this chapter, represent vertical walls, viewed from directly above.) Alternatively, visibility determination can be performed by front-to-back traversal working in conjunction with some method for remembering which pixels have already been drawn. The latter approach is more complex, but has the potential benefit of allowing you to early-out from traversal of the scene database when all the pixels on the screen have been drawn.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/59-02.html b/59-02.html index feb89e9..364fc98 100644 --- a/59-02.html +++ b/59-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Back-to-front or front-to-back traversal in itself wouldn’t be so impressive—there are many ways to do that—were it not for one additional detail: The traversal can always be performed in linear time, as we’ll see later on. For instance, you can traverse, a polygon list back-to-front from any viewpoint simply by walking through the corresponding BSP tree once, visiting each node one and only one time, and performing only one relatively inexpensive test at each node.

It’s hard to get cheaper sorting than linear time, and BSP-based rendering stacks up well against alternatives such as z-buffering, octrees, z-scan sorting, and polygon sorting. Better yet, a scene database represented as a BSP tree can be clipped to the view pyramid very efficiently; huge chunks of a BSP tree can be lopped off when clipping to the view pyramid, because if the entire area or volume of a node lies entirely outside the view volume, then all nodes and leaves that are children of that node must likewise be outside the view volume, for reasons that will become clear as we delve into the workings of BSP trees.

@@ -37,7 +40,7 @@


Figure 59.1
  The painter’s algorithm.

-

Limitations of BSP Trees

+

Limitations of BSP Trees

Powerful as they are, BSP trees aren’t perfect. By far the greatest limitation of BSP trees is that they’re time-consuming to build, enough so that, for all practical purposes, BSP trees must be precalculated, and cannot be built dynamically at runtime. In fact, a BSP-tree compiler that attempts to perform some optimization (limiting the number of surfaces that need to be split, for example) can easily take minutes or even hours to process large world databases.

@@ -62,16 +65,20 @@

There are infinitely valid ways to carve up Figure 59.2, but the simplest is just to carve along the lines of the walls themselves, with each node containing one wall. This is not necessarily optimal, in the sense of producing the smallest tree, but it has the virtue of generating the splitting lines without expensive analysis. It also saves on data storage, because the data for the walls can do double duty in describing the splitting lines as well. (Putting one wall on each splitting line doesn’t actually create a unique subspace for each wall, but it does create a unique subspace boundary for each wall; as we’ll see, that spatial organization provides for the same unambiguous visibility ordering as a unique subspace would.)

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/59-03.html b/59-03.html index 9cf7064..2090fd0 100644 --- a/59-03.html +++ b/59-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Creating a BSP tree is a recursive process, so we’ll perform the first split and go from there. Figure 59.3 shows the world carved along the line of wall C into two parts: walls that are in front of wall C, and walls that are behind. (Any of the walls would have been an equally valid choice for the initial split; we’ll return to the issue of choosing splitting walls in the next chapter.) This splitting into front and back is the essential dualism of BSP trees.


@@ -47,7 +50,7 @@

Both, actually. Wall A gets split into two pieces, which I’ll call wall A and wall E; each piece is assigned to the appropriate subspace and treated as a separate wall. As shown in Figure 59.6, each of the split pieces then has a subspace to itself, and each becomes a leaf of the tree. The BSP tree is now complete.

-

Visibility Ordering

+

Visibility Ordering

Now that we’ve successfully built a BSP tree, you might justifiably be a little puzzled as to how any of this helps with visibility ordering. The answer is that each BSP node can definitively determine which of its child trees is nearer and which is farther from any and all viewpoints; applied throughout the tree, this principle makes it possible to establish visibility ordering for all the line segments or planes in a BSP tree, no matter what the viewing angle.

@@ -102,16 +105,20 @@ void WalkBSPTree(NODE *pNode) -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/59-04.html b/59-04.html index c9c5d72..6bd6a6f 100644 --- a/59-04.html +++ b/59-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Inorder Walks of BSP Trees

It was implementing BSP trees that got me to thinking about inorder tree traversal. In inorder traversal, the left subtree of each node gets visited first, then the node, and then the right subtree. You apply this sequence recursively to each node and its children until the entire tree has been visited, as shown in Figure 59.9. Walking a BSP tree is basically an inorder tree walk; the only difference is that with a BSP tree a decision is made before each descent as to which subtree to visit first, rather than simply visiting whatever’s pointed to by the left-subtree pointer. Conceptually, however, an inorder walk is what’s used to traverse a BSP tree; from now on I’ll discuss normal inorder walking, with the understanding that the same principles apply to BSP trees.

@@ -87,7 +90,7 @@ struct _NODE *pRightChild;

And yet, a data-recursive inorder walk implementation has exactly the same flowchart and exactly the same functionality as the code-recursive version they’ve already written. They already have a fully functional model to follow, with all the problems solved, but they can’t make the connection between that model and the code they’re trying to implement. Why is this?

-

Know It Cold

+

Know It Cold

The problem is that these people don’t understand inorder walking through and through. They understand the concepts of visiting left and right subtrees, and they have a general picture of how traversal moves about the tree, but they do not understand exactly what the code-recursive version does. If they really comprehended everything that happens in each iteration of WalkTree()—how each call saves the state, and what that implies for the order in which operations are performed—they would simply and without fuss implement code like that in Listing 59.4, working with the code-recursive version as a model.

@@ -165,16 +168,20 @@ void WalkTree(NODE *pNode)
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/59-05.html b/59-05.html index fde8a9f..dce6090 100644 --- a/59-05.html +++ b/59-05.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Take a few minutes to look over Listing 59.4 and relate it to Listing 59.2. The structure is different, but upon examination it becomes clear that both listings reflect the same underlying model: For each node, visit the left subtree, visit the node, visit the right subtree. And although Listing 59.4 is longer, that’s mostly because I commented it heavily to make sure its workings are understood; there are only 13 lines that actually do anything in Listing 59.4.

Let’s look at it another way. All the code in Listing 59.2 does is say: “Here I am at a node. First I’ll visit the left subtree if there is one, then I’ll visit this node, then I’ll visit the right subtree if there is one. While I’m visiting the left subtree, I’ll just push a marker on a stack that tells me to come back here when the left subtree is done. If, after visiting a node, there are no right children to visit and nothing left on the stack, I’m finished. The code does this at each node—and that’s all it does. That’s all Listing 59.4 does, too, but people tend to get tangled up in pushes and pops and while loops when they use data recursion. When the implementation model changes to one with which they are unfamiliar, they abandon the perfectly good model they used before and try to rederive it in the new context by the seat of their pants.

@@ -48,7 +51,7 @@ -

Measure and Learn

+

Measure and Learn

How much difference does all this fuss make, anyway? Listing 59.5 is a sample program that builds a tree, then calls WalkTree () to walk it 1,000 times, and times how long this takes. Using 32-bit Visual C++ 1.10 running on Windows NT, with default optimization selected, Listing 59.5 reports that Listing 59.4 is about 20 percent faster than Listing 59.2 on a 486/33, a reasonable return for a little code rearrangement, especially when you consider that the speedup is diluted by calling the Visit() function and by the cache miss that happens on virtually every node access. (Listing 59.5 builds a rather unique tree, one in which every node has exactly two children. Different sorts of trees can and do produce different performance results. Always know what you’re measuring!)

@@ -123,16 +126,20 @@ void Visit(NODE *pNode) -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/59-06.html b/59-06.html index ae62af0..895fc40 100644 --- a/59-06.html +++ b/59-06.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Things change when maximum optimization is selected, however: The performance of the two implementations becomes virtually identical! How can this be? Part of the answer is that the compiler does an amazingly good job with Listing 59.2. Most impressively, when compiling Listing 59.2, the compiler actually converts all right-subtree descents from code recursion to data recursion, by simply jumping back to the left-subtree handling code instead of recursively calling WalkTree(). This means that half the time Listing 59.4 has no advantage over Listing 59.2; in fact, it’s at a disadvantage because the code that the compiler generates for handling right-subtree descent in Listing 59.4 is somewhat inefficient, but the right-subtree code in Listing 59.2 is a marvel of code generation, at just 3 instructions.

What’s more, although left-subtree traversal is more efficient with data recursion than with code recursion, the advantage is only four instructions, because only one parameter is passed and because the compiler doesn’t bother setting up an EBP-based stack frame, instead it uses ESP to address the stack. (And, in fact, this cost could be reduced still further by eliminating the check for a NULL pNode at all but the top level.) There are other interesting aspects to what the compiler does with Listings 59.2 and 59.4 but that’s enough to give you the idea. It’s worth noting that the compiler might not do as well with code recursion in a more complex function, and that a good assembly language implementation could probably speed up Listing 59.4 enough to make it measurably faster than Listing 59.2, but not even close to being enough faster to be worth the effort.

@@ -60,7 +63,7 @@

In the next chapter, we’ll build a BSP-tree compiler, and after that, we’ll put together a rendering system built around the BSP trees the compiler generates. If the subject of BSP trees really grabs your fancy (as it should if you care at all about performance graphics) there is at this writing (February 1996) a World Wide Web page on BSP trees that you must investigate at http://www.qualia.com/bspfaq/. It’s set up in the familiar Internet Frequently Asked Questions (FAQ) style, and is very good stuff.

-

Related Reading

+

Related Reading

Foley, J., A. van Dam, S. Feiner, and J. Hughes, Computer Graphics: Principles and Practice (Second Edition), Addison Wesley, 1990, pp. 555-557, 675-680.

@@ -70,16 +73,20 @@

Naylor, B., “Binary Space Partitioning Trees as an Alternative Representation of Polytopes,” Computer Aided Design, Vol. 22(4), May 1990, pp. 250-253.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/60-01.html b/60-01.html index bc2a7b9..57ab544 100644 --- a/60-01.html +++ b/60-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 60
Compiling BSP Trees

@@ -59,16 +62,20 @@

As you’ll recall from the previous chapter, a BSP tree is nothing more than a series of binary subdivisions that partion space into ever-smaller pieces. That’s a simple data structure, and a BSP compiler is a correspondingly simple tool. First, it groups all the surfaces (lines in 2-D, or polygons in 3-D) together into a single subspace that encompasses the entire world of the database. Then, it chooses one of the surfaces as the root node, and uses its line or plane to divide the remaining surfaces into two subspaces, splitting surfaces into two parts if they cross the line or plane of the root. Each of the two resultant subspaces is then processed in the same fashion, and so on, recursively, until the point is reached where all surfaces have been assigned to nodes, and each leaf surface subdivides a subspace that is empty except for that surface. Put another way, the root node carves space into two parts, and the root’s children carve each of those parts into two more parts, and so on, with each surface carving ever smaller subspaces, until all surfaces have been used. (Actually, there are many other lines or planes that a BSP tree can use to carve up space, but this is the approach we’ll use in the current discussion.)

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/60-02.html b/60-02.html index 9286d40..512db90 100644 --- a/60-02.html +++ b/60-02.html @@ -1,5 +1,4 @@ - + @@ -19,22 +18,26 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

If you find any of the above confusing (and it would be understandable if that were the case; BSP trees are not easy to get the hang of), you might want to refer back to the previous chapter. It would also be a good idea to get hold of the visual BSP compiler I’ll discuss shortly; when it comes to understanding BSP trees, there’s nothing quite like seeing one being built.

So there are really only two interesting operations in building a BSP tree: choosing a root node for the current subspace (a “splitter”) and assigning surfaces to one side or another of the current root node, splitting any that straddle the splitter. We’ll get to the issue of choosing splitters shortly, but first let’s look at the process of splitting and assigning. To do that, we need to understand parametric lines.

-

Parametric Lines

+

Parametric Lines

We’re all familiar with lines described in slope-intercept form, with y as a function of x

@@ -63,7 +66,7 @@


Figure 60.2
  Line segment storage in the BSP compiler.

-

Parametric Line Clipping

+

Parametric Line Clipping

In order to assign a line segment to one subspace or the other of a splitter, we must somehow figure out whether the line segment straddles the splitter or falls on one side or the other. In order to determine that, we first plug the line segment and splitter into the following parametric line intersection equation

@@ -80,20 +83,24 @@

One interesting point about Listing 60.1 is that it generates normals to splitting surfaces simply by exchanging the x and y lengths of the splitting line segment and negating the resultant y value, thereby rotating the line 90 degrees. In 3-D, it’s not that simple to come by a normal; you could calculate the normal as the cross-product of two of the polygon’s edges, or precalculate it when you build the world database.

-

The BSP Compiler

+

The BSP Compiler

Listing 60.1 shows the core of a BSP compiler—the code that actually builds the BSP tree. (Note that Listing 60.1 is excerpted from a C++ .CPP file, but in fact what I show here is very close to straight C. It may even compile as a .C file, though I haven’t checked.) The compiler begins by setting up an empty tree, then passes that tree and the complete set of line segments from which a BSP tree is to be generated to SelectBSPTree(), which chooses a root node and calls BuildBSPTree() to add that node to the tree and generate child trees for each of the node’s two subspaces. BuildBSPTree() calls SelectBSPTree() recursively to select a root node for each of those child trees, and this continues until all lines have been assigned nodes. SelectBSP() uses parametric clipping to decide on the splitter, as described below, and BuildBSPTree() uses parametric clipping to decide which subspace of the splitter each line belongs in, and to split lines, if necessary.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/60-03.html b/60-03.html index 7ad4e56..bdc4f87 100644 --- a/60-03.html +++ b/60-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 60.1 L60_1.CPP

 #define MAX_NUM_LINESEGS 1000
@@ -291,16 +294,20 @@ LINESEG * BuildBSPTree(LINESEG * plineseghead, LINESEG * prootline,
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/60-04.html b/60-04.html index 2df1b8f..60607f6 100644 --- a/60-04.html +++ b/60-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 60.1 isn’t very long or complex, but it’s somewhat more complicated than it could be because it’s structured to allow visual display of the ongoing compilation process. That’s because Listing 60.1 is actually just a part of a BSP compiler for Win32 that visually depicts the progressive subdivision of space as the BSP tree is built. (Note that Listing 60.1 might not compile as printed; I may have missed copying some global variables that it uses.) The complete code is too large to print here in its entirety, but it’s on the CD-ROM in file DDJBSP.ZIP.

Optimizing the BSP Tree

@@ -50,16 +53,20 @@

Although BSP trees have been around for at least 15 years now, they’re still only partially understood and are a ripe area for applied research and general ingenuity. You might want to try your hand at inventing new BSP optimization approaches; it’s an interesting problem, and you might strike paydirt. There are many things that BSP trees can’t do well, because it takes so long to build them—but what they do, they do exceedingly well, so a better compilation approach that allowed BSP trees to be used for more purposes would be valuable, indeed.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/61-01.html b/61-01.html index 26e52d9..b103dad 100644 --- a/61-01.html +++ b/61-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 61
Frames of Reference

@@ -47,7 +50,7 @@

Before we can talk about transforming between coordinate spaces, however, we need two building blocks: dot products and cross products.

-

3-D Math

+

3-D Math

At this point in the book, I was originally going to present a BSP-based renderer, to complement the BSP compiler I presented in the previous chapter. What changed my plans was the considerable amount of mail about 3-D math that I’ve gotten in recent months. In every case, the writer has bemoaned his/her lack of expertise with 3-D math, and has asked what books about 3-D math I’d recommend, and how else he/she could learn more.

@@ -55,7 +58,7 @@

The other thing the mail made clear was that there are a lot of people out there who don’t understand either type of product, at least insofar as they apply to 3-D. Since much or even most advanced 3-D graphics machinery relies to a greater or lesser extent on dot products and cross products (even the line intersection formula I discussed in the last chapter is actually a quotient of dot products), I’m going to spend this chapter examining these basic tools and some of their 3-D applications. If this is old hat to you, my apologies, and I’ll return to BSP-based rendering in the next chapter.

-

Foundation Definitions

+

Foundation Definitions

The dot and cross products themselves are straightforward and require almost no context to understand, but I need to define some terms I’ll use when describing applications of the products, so I’ll do that now, and then get started with dot products.

@@ -71,16 +74,20 @@

(where vertical double bars denote vector length), and a direction in the plane of the x and z axes, exactly halfway between those two axes.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/61-02.html b/61-02.html index 9266d16..4fc7d8a 100644 --- a/61-02.html +++ b/61-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

I’ll be working in a left-handed coordinate system, whereby if you wrap the fingers of your left hand around the z axis with your thumb pointing in the positive z direction, your fingers will curl from the positive x axis to the positive y axis. The positive x axis runs left to right across the screen, the positive y axis runs bottom to top across the screen, and the positive z axis runs into the screen.

For our purposes, projection is the process of mapping coordinates onto a line or surface. Perspective projection projects 3-D coordinates onto a viewplane, scaling coordinates according to their z distance from the viewpoint in order to provide proper perspective. Objectspace is the coordinate space in which an object is defined, independent of other objects and the world itself. Worldspace is the absolute frame of reference for a 3-D world; all objects’ locations and orientations are with respect to worldspace, and this is the frame of reference around which the viewpoint and view direction move. Viewspace is worldspace as seen from the viewpoint, looking in the view direction. Screenspace is viewspace after perspective projection and scaling to the screen.

@@ -42,8 +45,6 @@

Now we’re ready to move on to the dot product. Given two vectors U = [u1 u2 u3] and V = [v1 v2 v3], their dot product, denoted by the symbol •, is calculated as:

-

-

(eq. 2)

@@ -52,20 +53,16 @@

Now that we know how to calculate a dot product, what does that get us? Not much. The dot product isn’t of much use for graphics until you start thinking of it this way

-

-

(eq. 3)

where q is the angle between the two vectors, and the other two terms are the lengths of the vectors, as shown in Figure 61.1. Although it’s not immediately obvious, equation 3 has a wide variety of applications in 3-D graphics.

-

Dot Products of Unit Vectors

+

Dot Products of Unit Vectors

The simplest case of the dot product is when both vectors are unit vectors; that is, when their lengths are both one, as calculated as in Equation 1. In this case, equation 3 simplifies to:

-

-

(eq. 4)

@@ -74,8 +71,6 @@

One obvious use of this is to find angles between unit vectors, in conjunction with an inverse cosine function or lookup table. A more useful application in 3-D graphics lies in lighting surfaces, where the cosine of the angle between incident light and the normal (perpendicular vector) of a surface determines the fraction of the light’s full intensity at which the surface is illuminated, as in

-

-

(eq. 5)

@@ -85,8 +80,6 @@

where Is is the intensity of illumination of the surface, Il is the intensity of the light, and q is the angle between -Dl (where Dl is the light direction vector) and the surface normal. If the inverse light vector and the surface normal are both unit vectors, then this calculation can be performed with four multiplies and three additions—and no explicit cosine calculations—as

-

-

(eq. 6)

@@ -102,16 +95,20 @@


Figure 61.2
  The dot product as used in calculating lighting intensity.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/61-03.html b/61-03.html index 26bc3b0..dd36078 100644 --- a/61-03.html +++ b/61-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Why does this matter? It matters because, on average, half the polygons in any scene are facing away from the viewer, and hence shouldn’t be drawn. One way to identify such polygons is to see whether they’re facing toward or away from the viewer; that is, whether their normals have negative z values (so they’re visible) or positive z values (so they should be culled). However, we’re talking about screenspace normals here, because the perspective projection can shift a polygon relative to the viewpoint so that although its viewspace normal has a negative z, its screenspace normal has a positive z, and vice-versa, as shown in Figure 61.3. So we need screenspace normals, but those can’t readily be generated by transformation from worldspace.


@@ -37,8 +40,6 @@

The solution is to use the cross product of two of the polygon’s edges to generate a normal. The formula for the cross product is:

-

-

(eq. 7)

@@ -73,16 +74,20 @@


Figure 61.5
  Backface culling with the dot product.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/61-04.html b/61-04.html index b356a95..71519d4 100644 --- a/61-04.html +++ b/61-04.html @@ -1,5 +1,4 @@ - + @@ -19,23 +18,25 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Using the Dot Product for Projection

Consider Equation 3 again, but this time make one of the vectors, say V, a unit vector. Now the equation reduces to:

-

-

(eq. 8)

@@ -109,16 +110,20 @@ void LineIntersectPlane (float *linestart, float *lineend,


Figure 61.8
  Rotation to a new coordinate space by projection onto new axes.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/62-01.html b/62-01.html index 13bf7c8..f492992 100644 --- a/62-01.html +++ b/62-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 62
One Story, Two Rules, and a BSP Renderer

@@ -59,7 +62,7 @@

Onward to rendering from a BSP tree.

-

BSP-based Rendering

+

BSP-based Rendering

For the last several chapters I’ve been discussing the nature of BSP (Binary Space Partitioning) trees, and in Chapter 60 I presented a compiler for 2-D BSP trees. Now we’re ready to use those compiled BSP trees to do realtime rendering.

@@ -72,16 +75,20 @@

Given a BSP tree, in order to render a view of that tree, all we have to do is descend the tree, deciding at each node whether we’re seeing the front or back of the wall at that node from the current viewpoint. We use that knowledge to first recursively descend and draw the farther subtree of that node, then draw that node, and finally draw the nearer subtree of that node. Applied recursively from the root of our BSP trees, this approach guarantees that overlapping polygons will always be drawn in back-to-front order. Listing 62.1 draws a BSP-based world in this fashion. (Because of the constraints of the printed page, Listing 62.1 is only the core of the BSP renderer, without the program framework, some math routines, and the polygon rasterizer; but, the entire program is on the CD-ROM as DDJBSP2.ZIP. Listing 62.1 is in a compressed format, with relatively little whitespace; the full version on the CD-ROM is formatted normally.)

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/62-02.html b/62-02.html index 060f748..5b65d96 100644 --- a/62-02.html +++ b/62-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 62.1 L62_1.C

 /* Core renderer for Win32 program to demonstrate drawing from a 2-D
@@ -473,16 +476,20 @@ void UpdateWorld()
 }
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/62-03.html b/62-03.html index 93c720c..7d4b53f 100644 --- a/62-03.html +++ b/62-03.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

The Rendering Pipeline

+

The Rendering Pipeline

Conceptually rendering from a BSP tree really is that simple, but the implementation is a bit more complicated. The full rendering pipeline, as coordinated by UpdateWorld(), is this:

@@ -84,16 +87,20 @@


Figure 62.3
  Why y clipping is more complex than x or z clipping.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/62-04.html b/62-04.html index 6381196..36b0f22 100644 --- a/62-04.html +++ b/62-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Walking the Tree, Backface Culling and Drawing

Now that we have all the walls clipped to the frustum, with vertices projected into screen coordinates, all we have to do is draw them back to front; that’s the job of DrawWallsBackToFront(). Basically, this routine walks the BSP tree, descending recursively from each node to draw the farther children of each node first, then the wall at the node, then the nearer children. In the interests of efficiency, this particular implementation performs a data-recursive walk of the tree, rather than the more familiar code recursion. Interestingly, the performance speedup from data recursion turned out to be more modest than I had expected, based on past experience; see Chapter 59 for further details.

@@ -47,7 +50,7 @@


Figure 62.4
  Fast backspace culling test in screenspace.

-

Notes on the BSP Renderer

+

Notes on the BSP Renderer

Listing 62.1 is far from complete or optimal. There is no such thing as a tiny BSP rendering demo, because 3D rendering, even when based on a 2-D BSP tree, requires a substantial amount of code and complexity. Listing 62.1 is reasonably close to a minimum rendering engine, and is specifically intended to illuminate basic BSP principles, given the space limitations of one chapter in a book that’s already larger than it should be. Think of Listing 62.1 as a learning tool and a starting point.

@@ -57,16 +60,20 @@

This sort of BSP tree, organized around volumes rather than polygons, has some additional interesting advantages in simulating physics, detecting collisions, doing line-of-sight determination, and performing volume-based operations such as dynamic illumination and event triggering. However, that discussion will have to wait until another day.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/63-01.html b/63-01.html index 177f698..664d933 100644 --- a/63-01.html +++ b/63-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 63
Floating-Point for Real-Time 3-D

@@ -77,16 +80,20 @@

I’m going to focus on six core instructions in this section: FLD, FST, FADD, FSUB, FMUL, and FDIV. First, let’s look at cycle times for these instructions. FLD takes 1 cycle; the value is pushed onto the FP stack and ready for use on the next cycle. FST takes 2 cycles, although when storing to memory, there’s a potential extra cycle that can be lost, as I’ll describe shortly.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/63-02.html b/63-02.html index 7d63d7b..1810e5d 100644 --- a/63-02.html +++ b/63-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

FDIV is a painfully slow instruction, taking 39 cycles at full precision and 33 cycles at double precision, which is the default precision for Visual C++ 2.0. While FDIV executes, the FPU is occupied, and can’t process subsequent FP instructions until FDIV finishes. However, during the cycles while FDIV is executing (with the exception of the one cycle during which FDIV starts), the integer unit can simultaneously execute instructions other than IMUL. (IMUL uses the FPU, and can only overlap with FDIV for a few cycles.) Since the integer unit can execute two instructions per cycle, this means it’s possible to have three instructions, an FDIV and two integer instructions, executing at the same time. That’s exactly what happens, for example, during the second cycle of this code:

 FDIV ST(0),ST(1)
@@ -47,7 +50,7 @@ FST  [temp]
 
   

takes 6 cycles in all.) Again, it’s possible to execute integer-unit instructions during the 2 (or 3, for FST) cycles after one of these FP instructions starts. There’s a more exciting possibility here, though: Given properly structured code, the FPU is capable of averaging 1 cycle per FADD, FSUB, or FMUL. The secret is pipelining.

-

Pipelining, Latency, and Throughput

+

Pipelining, Latency, and Throughput

The Pentium’s FPU is the first pipelined x86 FPU. Pipelining means that the FPU is capable of starting an instruction every cycle, and can simultaneously handle several instructions in various stages of completion. Only certain x86 FP instructions allow another instruction to start on the next cycle, though: FADD, FSUB, and FMUL are pipelined, but FST and FDIV are not. (FLD executes in a single cycle, so pipelining is not an issue.) Thus, in the code sequence

@@ -85,7 +88,7 @@ FSUB ST(0),ST(1)
 
   

where the ST(0) operand to FSUB is calculated by FADD. Here, FSUB can’t start until FADD has completed, so there are 2 stall cycles between the two instructions. When dependencies like this occur, the FPU runs at latency rather than throughput speeds, and performance can drop by as much as two-thirds.

-

FXCH

+

FXCH

One piece of the puzzle is still missing. Clearly, to get maximum throughput, we need to interleave FP instructions, such that at any one time ideally three instructions are in the pipeline at once. Further, these instructions must not depend on one another for operands. But ST(0) must always be one of the operands; worse, FLD can only push into ST(0), and FST can only store from ST(0). How, then, can we keep three independent instructions going?

@@ -110,16 +113,20 @@ FSUB ST(0),ST(1)

Listing 63.2 shows a straightforward dot product implementation. This version loses 7 cycles to stalls. Listing 63.3 cuts the loss to 5 cycles by doing all three FMULs first, then using FXCH to set the third FXCH aside to complete while the results of the first two FMULs, which have completed, are added. Listing 43.3 still loses 50 percent to stalls, but unless some other code is available to be interleaved with the dot product code, that’s all we can do to speed things up. Fortunately, dot products are often used in contexts where there’s plenty of interleaving potential, as we’ll see when we discuss transformation.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/63-03.html b/63-03.html index 4d10c54..e5e6d25 100644 --- a/63-03.html +++ b/63-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Listing 63.2 1 L63-2.ASM

 ; unoptimized dot product; 17 cycles
@@ -155,16 +158,20 @@ v2 = m21u1 + m22u2 + m3 = m31u1 + m32u2 + m33u3 + m34.
 
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/63-04.html b/63-04.html index 327476a..c8497fd 100644 --- a/63-04.html +++ b/63-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

When it comes to implementation, however, transformation is quite different from three separate dot products and additions, because once again the magic number three is involved. Three separate dot products and additions would take 60 cycles if each were calculated using the unoptimized dot-product code of Listing 63.2, and would take 54 cycles if done one after the other using the faster dot-product code of Listing 63.3, in each case followed by the a final addition per dot product.

When fully interleaved, however, only a single cycle is lost (again to the extra cycle of FST latency), and the cycle count drops to 34, as shown in Listing 63.6. This means that on a 100 MHz Pentium, it’s theoretically possible to do nearly 3,000,000 transforms per second, although that’s a purely hypothetical number, due to cache effects and set-up costs. Still, more than 1,000,000 transforms per second is certainly feasible; at a frame rate of 30 Hz, that’s an impressive 30,000 transforms per frame.

@@ -96,16 +99,20 @@

And I won’t miss it a bit.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/64-01.html b/64-01.html index c391098..3f9b0d9 100644 --- a/64-01.html +++ b/64-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 64
Quake’s Visible-Surface Determination

@@ -69,16 +72,20 @@

In contrast, VSD is an open-ended problem, and there are dozens of approaches currently in use. Even more significantly, the performance of VSD, done in an unsophisticated fashion, scales directly with scene complexity, which tends to increase as a square or cube function, so this very rapidly becomes the limiting factor in rendering realistic worlds. I expect VSD to be the increasingly dominant issue in realtime PC 3-D over the next few years, as 3-D worlds become increasingly detailed. Already, a good-sized Quake level contains on the order of 10,000 polygons, about three times as many polygons as a comparable DOOM level.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/64-02.html b/64-02.html index d4cfd69..b75d1d5 100644 --- a/64-02.html +++ b/64-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The Structure of Quake Levels

Before diving into VSD, let me note that each Quake level is stored as a single huge 3-D BSP tree. This BSP tree, like any BSP, subdivides space, in this case along the planes of the polygons. However, unlike the BSP tree I presented in Chapter 62, Quake’s BSP tree does not store polygons in the tree nodes, as part of the splitting planes, but rather in the empty (non-solid) leaves, as shown in overhead view in Figure 64.1.

@@ -52,7 +55,7 @@

For relatively simple worlds, it is perfectly acceptable. It doesn’t scale very well, though. One problem is that as you add more polygons in the world, more transformations and tests have to be performed to cull polygons that aren’t visible; at some point, that will bog considerably performance down.

-

Nodes Inside and Outside the View Frustum

+

Nodes Inside and Outside the View Frustum

Happily, there’s a good workaround for this particular problem. As discussed earlier, each leaf of a BSP tree represents a convex subspace, with the nodes that bound the leaf delimiting the space. Perhaps less obvious is that each node in a BSP tree also describes a subspace—the subspace composed of all the node’s children, as shown in Figure 64.3. Another way of thinking of this is that each node splits the subspace into two pieces created by the nodes above it in the tree, and the node’s children then further carve that subspace into all the leaves that descend from the node.

@@ -73,16 +76,20 @@

By three months after I arrived, only one element of the original VSD design was anywhere in sight, and John had taken the dictum of “try new things” farther than I’d ever seen it taken.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/64-03.html b/64-03.html index 9294f85..4aaf604 100644 --- a/64-03.html +++ b/64-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The Beam Tree

John’s original Quake design was to draw front-to-back, using a second BSP tree to keep track of what parts of the screen were already drawn and which were still empty and therefore drawable by the remaining polygons. Logically, you can think of this BSP tree as being a 2-D region describing solid and empty areas of the screen, as shown in Figure 64.4, but in fact it is a 3-D tree, of the sort known as a beam tree. A beam tree is a collection of 3-D wedges (beams), bounded by planes, projecting out from some center point, in this case the viewpoint, as shown in Figure 64.5.

@@ -50,23 +53,23 @@

Once the beam tree was working, John relentlessly worked at speeding up the 3-D engine, always trying to improve the design, rather than tweaking the implementation. At least once a week, and often every day, he would walk into my office and say “Last night I couldn’t get to sleep, so I was thinking...” and I’d know that I was about to get my mind stretched yet again. John tried many ways to improve the beam tree, with some success, but more interesting was the profusion of wildly different approaches that he generated, some of which were merely discussed, others of which were implemented in overnight or weekend-long bursts of coding, in both cases ultimately discarded or further evolved when they turned out not to meet the design criteria well enough. Here are some of those approaches, presented in minimal detail in the hopes that, like Tom Wilson with the Paradise FIFO, your imagination will be sparked.

-

Subdividing Raycast

+

Subdividing Raycast

Rays are cast in an 8x8 screen-pixel grid; this is a highly efficient operation because the first intersection with a surface can be found by simply clipping the ray into the BSP tree, starting at the viewpoint, until a solid leaf is reached. If adjacent rays don’t hit the same surface, then a ray is cast halfway between, and so on until all adjacent rays either hit the same surface or are on adjacent pixels; then the block around each ray is drawn from the polygon that was hit. This scales very well, being limited by the number of pixels, with no overdraw. The problem is dropouts; it’s quite possible for small polygons to fall between rays and vanish.

-

Vertex-Free Surfaces

+

Vertex-Free Surfaces

The world is represented by a set of surface planes. The polygons are implicit in the plane intersections, and are extracted from the planes as a final step before drawing. This makes for fast clipping and a very small data set (planes are far more compact than polygons), but it’s time-consuming to extract polygons from planes.

-

The Draw-Buffer

+

The Draw-Buffer

Like a z-buffer, but with 1 bit per pixel, indicating whether the pixel has been drawn yet. This eliminates overdraw, but at the cost of an inner-loop buffer test, extra writes and cache misses, and, worst of all, considerable complexity. Variations include testing the draw-buffer a byte at a time and completely skipping fully-occluded bytes, or branching off each draw-buffer byte to one of 256 unrolled inner loops for drawing 0-8 pixels, in the process possibly taking advantage of the ability of the x86 to do the perspective floating-point divide in parallel while 8 pixels are processed.

-

Span-Based Drawing

+

Span-Based Drawing

Polygons are rasterized into spans, which are added to a global span list and clipped against that list so that only the nearest span at each pixel remains. Little sorting is needed with front-to-back walking, because if there’s any overlap, the span already in the list is nearer. This eliminates overdraw, but at the cost of a lot of span arithmetic; also, every polygon still has to be turned into spans.

-

Portals

+

Portals

The holes where polygons are missing on surfaces are tracked, because it’s only through such portals that line-of-sight can extend. Drawing goes front-to-back, and when a portal is encountered, polygons and portals behind it are clipped to its limits, until no polygons or portals remain visible. Applied recursively, this allows drawing only the visible portions of visible polygons, but at the cost of a considerable amount of portal clipping.

@@ -74,16 +77,20 @@

In the end, John decided that the beam tree was a sort of second-order structure, reflecting information already implicitly contained in the world BSP tree, so he tackled the problem of extracting visibility information directly from the world BSP tree. He spent a week on this, as a byproduct devising a perfect DOOM (2-D) visibility architecture, whereby a single, linear walk of a DOOM BSP tree produces zero-overdraw 2-D visibility. Doing the same in 3-D turned out to be a much more complex problem, though, and by the end of the week John was frustrated by the increasing complexity and persistent glitches in the visibility code. Although the direct-BSP approach was getting closer to working, it was taking more and more tweaking, and a simple, clean design didn’t seem to be falling out. When I left work one Friday, John was preparing to try to get the direct-BSP approach working properly over the weekend.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/64-04.html b/64-04.html index 3488b36..03ff1ae 100644 --- a/64-04.html +++ b/64-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

When I came in on Monday, John had the look of a man who had broken through to the other side—and also the look of a man who hadn’t had much sleep. He had worked all weekend on the direct-BSP approach, and had gotten it working reasonably well, with insights into how to finish it off. At 3:30 Monday morning, as he lay in bed, thinking about portals, he thought of precalculating and storing in each leaf a list of all leaves visible from that leaf, and then at runtime just drawing the visible leaves back-to-front for whatever leaf the viewpoint happens to be in, ignoring all other leaves entirely.

Size was a concern; initially, a raw, uncompressed potentially visible set (PVS) was several megabytes in size. However, the PVS could be stored as a bit vector, with 1 bit per leaf, a structure that shrunk a great deal with simple zero-byte compression. Those steps, along with changing the BSP heuristic to generate fewer leaves (choosing as the next splitter the polygon that splits the fewest other polygons appears to be the best heuristic) and sealing the outside of the levels so the BSPer can remove the outside surfaces, which can never be seen, eventually brought the PVS down to about 20 Kb for a good-size level.

@@ -74,16 +77,20 @@

Teller, Seth, Visibility Preprocessing for Interactive Walkthroughs, SIGGRAPH 91 proceedings, pp. 61-69.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/65-01.html b/65-01.html index 9e3b84a..caa6c90 100644 --- a/65-01.html +++ b/65-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 65
3-D Clipping and Other Thoughts

@@ -65,16 +68,20 @@

In a commercial application, you wouldn’t want to clip every single polygon in the scene database individually. As I mentioned in the last chapter, the use of bounding volumes to cull chunks of the scene database that fall entirely outside the frustum, without having to consider each polygon separately, is an important performance aspect of scene rendering. Once that’s done, however, you’re still left with a set of polygons that may be entirely inside, or partially or completely outside, the frustum. In this chapter, I’m going to talk about how to clip those remaining polygons. I’ll focus on the basics of 3-D clipping, the stuff I wish I’d known when I started doing 3-D. There are plenty of ways to speed up clipping under various circumstances, some of which I’ll mention, but the material covered below will give you the tools you need to implement functional 3-D clipping.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/65-02.html b/65-02.html index b59ac32..6256719 100644 --- a/65-02.html +++ b/65-02.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Intersecting a Line Segment with a Plane

+

Intersecting a Line Segment with a Plane

The fundamental 3-D clipping operation is clipping a line segment to a plane. There are two parts to this operation: determining if the line is clipped by (intersects) the plane at all and, if it is clipped, calculating the point of intersection.

@@ -94,16 +97,20 @@ typedef struct {


Figure 65.2
  Clipping a polygon.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/65-03.html b/65-03.html index 88427b7..966b776 100644 --- a/65-03.html +++ b/65-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

LISTING 65.2 L65_2.c

 int ClipToPlane(polygon_t *pin, plane_t *pplane, polygon_t *pout)
@@ -88,7 +91,7 @@ int ClipToPlane(polygon_t *pin, plane_t *pplane, polygon_t *pout)
 
   

One particularly useful aspect of 3-D clipping is that if you’re drawing texture mapped polygons, texture coordinates can be clipped in exactly the same way as (x,y,z) coordinates. In fact, the very same fraction that’s used to advance x, y, and z from the inside point to the point of intersection with the clip plane can be used to advance the texture coordinates as well, so only one extra multiply and one extra add are required for each texture coordinate.

-

Clipping to the Frustum

+

Clipping to the Frustum

Given a polygon-clipping function, it’s easy to clip to the frustum: set up the four planes for the sides of the frustum, with another one or two planes for near and far clipping, if desired; next, clip each potentially visible polygon to each plane in turn; then draw whatever polygons emerge from the clipping process. Listing 65.3 is the core code for a simple 3-D clipping example that allows you to move around and look at polygonal models from any angle. The full code for this program is available on the CD-ROM in the file DDJCLIP.ZIP.

@@ -405,16 +408,20 @@ void UpdateWorld() }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/65-04.html b/65-04.html index b64c231..18d2e92 100644 --- a/65-04.html +++ b/65-04.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

The Lessons of Listing 65.3

+

The Lessons of Listing 65.3

There are several interesting points to Listing 65.3. First, floating-point arithmetic is used throughout the clipping process. While it is possible to use fixed-point, doing so requires considerable care regarding range and precision. Floating-point is much easier—and, with the Pentium generation of processors, is generally comparable in speed. In fact, for some operations, such as multiplication in general and division when the floating-point unit is in single-precision mode, floating-point is much faster. Check out Chris Hecker’s column in the February 1996 Game Developer for an interesting discussion along these lines.

@@ -58,16 +61,20 @@

And, as you read, you might take a moment to consider how wonderful it is that anyone who’s interested can tap into so much expert knowledge for the price of a book—or, on the Internet, for free—with no strings attached. Our part of the world is a pretty good place right now, isn’t it?

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/66-01.html b/66-01.html index 9bb0242..11bb2ed 100644 --- a/66-01.html +++ b/66-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 66
Quake’s Hidden-Surface Removal

@@ -51,30 +54,34 @@

Back in Chapter 64, I described the creative flux that led to John Carmack’s decision to use a precalculated potentially visible set (PVS) of polygons for each possible viewpoint in Quake, the game we’re developing here at id Software. The precalculated PVS meant that instead of having to spend a lot of time searching through the world database to find out which polygons were visible from the current viewpoint, we could simply draw all the polygons in the PVS from back-to-front (getting the ordering courtesy of the world BSP tree) and get the correct scene drawn with no searching at all; letting the back-to-front drawing perform the final stage of hidden-surface removal (HSR). This was a terrific idea, but it was far from the end of the road for Quake’s design.

-

Drawing Moving Objects

+

Drawing Moving Objects

For one thing, there was still the question of how to sort and draw moving objects properly; in fact, this is the single technical question I’ve been asked most often in recent months, so I’ll take a moment to address it here. The primary problem is that a moving model can span multiple BSP leaves, with the leaves that are touched varying as the model moves; that, together with the possibility of multiple models in one leaf, means there’s no easy way to use BSP order to draw the models in correctly sorted order. When I wrote Chapter 64, we were drawing sprites (such as explosions), moveable BSP models (such as doors), and polygon models (such as monsters) by clipping each into all the leaves it touched, then drawing the appropriate parts as each BSP leaf was reached in back-to-front traversal. However, this didn’t solve the issue of sorting multiple moving models in a single leaf against each other, and also left some ugly sorting problems with complex polygon models.

John solved the sorting issue for sprites and polygon models in a startlingly low-tech way: We now z-buffer them. (That is, before we draw each pixel, we compare its distance, or z, value with the z value of the pixel currently on the screen, drawing only if the new pixel is nearer than the current one.) First, we draw the basic world, walls, ceilings, and the like. No z-buffer testing is involved at this point (the world visible surface determination is done in a different way, as we’ll see soon); however, we do fill the z-buffer with the z values (actually, 1/z values, as discussed below) for all the world pixels. Z-filling is a much faster process than z-buffering the entire world would be, because no reads or compares are involved, just writes of z values. Once the drawing and z-filling of the world is done, we can simply draw the sprites and polygon models with z-buffering and get perfect sorting all around.

-

Performance Impact

+

Performance Impact

Whenever a z-buffer is involved, the questions inevitably are: What’s the memory footprint and what’s the performance impact? Well, the memory footprint at 320x200 is 128K, not trivial but not a big deal for a game that requires 8 MB to run. The performance impact is about 10 percent for z-filling the world, and roughly 20 percent (with lots of variation) for drawing sprites and polygon models. In return, we get a perfectly sorted world, and also the ability to do additional effects, such as particle explosions and smoke, because the z-buffer lets us flawlessly sort such effects into the world. All in all, the use of the z-buffer vastly improved the visual quality and flexibility of the Quake engine, and also simplified the code quite a bit, at an acceptable memory and performance cost.

-

Leveling and Improving Performance

+

Leveling and Improving Performance

As I said above, in the Quake architecture, the world itself is drawn first, without z-buffer reads or compares, but filling the z-buffer with the world polygons’ z values, and then the moving objects are drawn atop the world, using full z-buffering. Thus far, I’ve discussed how to draw moving objects. For the rest of this chapter, I’m going to talk about the other part of the drawing equation; that is, how to draw the world itself, where the entire world is stored as a single BSP tree and never moves.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/66-02.html b/66-02.html index 5ae779f..bd7b832 100644 --- a/66-02.html +++ b/66-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

As you may recall from Chapter 64, we’re concerned with both raw performance and level performance. That is, we want the drawing code to run as fast as possible, but we also want the difference in drawing speed between the average scene and the slowest-drawing scene to be as small as possible.

@@ -70,16 +73,20 @@

With edge-sorting, edges are stored in x-sorted, linked list buckets according to their start scan line. Each polygon in turn is decomposed into edges, cumulatively building a list of all the edges in the scene. Once all edges for all polygons in the view frustum have been added to the edge list, the whole list is scanned out in a single top-to-bottom, left-to-right pass. An active edge list (AEL) is maintained. With each step to a new scan line, edges that end on that scan line are removed from the AEL, active edges are stepped to their new x coordinates, edges starting on the new scan line are added to the AEL, and the edges are sorted by current x coordinate.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/66-03.html b/66-03.html index 2325293..63d9951 100644 --- a/66-03.html +++ b/66-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

For each scan line, a z-sorted active polygon list (APL) is maintained. The x-sorted AEL is stepped through in order. As each new edge is encountered (that is, as each polygon starts or ends as we move left to right), the associated polygon is activated and sorted into the APL, as shown in Figure 66.3, or deactivated and removed from the APL, as shown in Figure 66.4, for a leading or trailing edge, respectively. If the nearest polygon has changed (that is, if the new polygon is nearest, or if the nearest polygon just ended), a span is emitted for the polygon that just stopped being the nearest, starting at the point where the polygon first because nearest and ending at the x coordinate of the current edge, and the current x coordinate is recorded in the polygon that is now the nearest. This saved coordinate later serves as the start of the span emitted when the new nearest polygon ceases to be in front.

Don’t worry if you didn’t follow all of that; the above is just a quick overview of edge-sorting to help make the rest of this chapter a little clearer. My thorough discussion of the topic will be in Chapter 67.

@@ -60,16 +63,20 @@

Getting z distances can be tricky, however. Remember that we need to be able to calculate z at any arbitrary point on a polygon, because an edge may occur and cause its polygon to be sorted into the APL at any point on the screen. We could calculate z directly from the screen x and y coordinates and the polygon’s plane equation, but unfortunately this can’t be done very quickly, because the z for a plane doesn’t vary linearly in screenspace; however, 1/z does vary linearly, so we’ll use that instead. (See Chris Hecker’s 1995 series of columns on texture mapping in Game Developer magazine for a discussion of screenspace linearity and gradients for 1/z.) Another advantage of using 1/z is that its resolution increases with decreasing distance, meaning that by using 1/z, we’ll have better depth resolution for nearby features, where it matters most.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/66-04.html b/66-04.html index 3b71764..51f8329 100644 --- a/66-04.html +++ b/66-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The obvious way to get a 1/z value at any arbitrary point on a polygon is to calculate 1/z at the vertices, interpolate it down both edges of the polygon, and interpolate between the edges to get the value at the point of interest. Unfortunately, that requires doing a lot of work along each edge, and worse, requires division to calculate the 1/z step per pixel across each span.

A better solution is to calculate 1/z directly from the plane equation and the screen x and y of the pixel of interest. The equation is

@@ -40,7 +43,7 @@

The full 1/z calculation requires two multiplies and two adds, all of which should be floating-point to avoid range errors. That much floating-point math sounds expensive but really isn’t, especially on a Pentium, where a plane’s 1/z value at any point can be calculated in as little as six cycles in assembly language.

-

Where That 1/Z Equation Comes From

+

Where That 1/Z Equation Comes From

For those who are interested, here’s a quick derivation of the 1/z equation. The plane equation for a plane is

@@ -56,7 +59,7 @@

We’ll see 1/z sorting in action in Chapter 67.

-

Quake and Z-Sorting

+

Quake and Z-Sorting

I mentioned earlier that Quake no longer uses BSP order as the sorting key; in fact, it uses 1/z as the key now. Elegant as the gradients are, calculating 1/z from them is clearly slower than just doing a compare on a BSP-ordered key, so why have we switched Quake to 1/z?

@@ -70,16 +73,20 @@

As I write this, it’s unclear whether Quake will end up sorting edges by BSP order or 1/z. Actually, there’s no guarantee that sorted spans in any form will be the final design. Sometimes it seems like we change graphics engines as often as they play Elvis on the ‘50s oldies stations (but, one would hope, with more aesthetically pleasing results!) and no doubt we’ll be considering the alternatives right up until the day we ship.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/67-01.html b/67-01.html index 68cbb68..ce63d41 100644 --- a/67-01.html +++ b/67-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 67
Sorted Spans in Action

@@ -57,16 +60,20 @@

That wasn’t the bad part; after all, even a small speed increase is A Good Thing. The real problem was that our initial 1/z sorting proved to be unreliable. We first ran into problems when two forward-facing polygons started at a common edge, because it was hard to tell which one was really in front (as discussed below), and we had to do additional floating-point calculations to resolve these cases. This fixed the problems for a while, but then odd cases started popping up where just the right combination of polygon alignments caused new sorting errors. We tinkered with those too, adding more code and incurring additional slowdowns in the process. Finally, we had everything working smoothly again, although by this point Quake was back to pretty much the same speed it had been with BSP sorting.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/67-02.html b/67-02.html index 3fbccfa..e098afa 100644 --- a/67-02.html +++ b/67-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

And then yet another crop of sorting errors popped up.

We could have fixed those errors too; we’ll take a quick look at how to deal with such cases shortly. However, like the sixth rocket stage, the fixes would have made Quake slower than it had been with BSP sorting. So we gave up and went back to BSP order, and now the code is simpler and sorting works reliably. It’s too bad our experiment didn’t work out, but it wasn’t wasted time because in trying what we did we learned quite a bit. In particular, we learned that the information provided by a simple, reliable world ordering mechanism, such as a BSP tree, can do more good than is immediately apparent, in terms of both performance and solid code.

@@ -44,7 +47,7 @@

There are three types of 1/z span sorting, each requiring a different implementation. In order of increasing speed and decreasing complexity, they are: intersecting, abutting, and independent. (These are names of my own devising; I haven’t come across any standard nomenclature in the literature.)

-

Intersecting Span Sorting

+

Intersecting Span Sorting

Intersecting span sorting occurs when polygons can interpenetrate. Thus, two spans may cross such that part of each span is visible, in which case the spans have to be split and drawn appropriately, as shown in Figure 67.1.

@@ -53,7 +56,7 @@

Intersecting is the slowest and most complicated type of span sorting, because it is necessary to compare 1/z values at two points in order to detect interpenetration, and additional work must be done to split the spans as necessary. Thus, although intersecting span sorting certainly works, it’s not the first choice for performance.

-

Abutting Span Sorting

+

Abutting Span Sorting

Abutting span sorting occurs when polygons that are not part of a continuous surface can butt up against one another, but don’t interpenetrate, as shown in Figure 67.2. This is the sorting used in Quake, where objects like doors often abut walls and floors, and turns out to be more complicated than you might think. The problem is that when an abutting polygon starts on a given scan line, as with polygon B in Figure 67.2, it starts at exactly the same 1/z value as the polygon it abuts, in this case, polygon A, so additional sorting is needed when these ties happen. Of course, the two-point sorting used for intersecting polygons would work, but we’d like to find something faster.

@@ -66,16 +69,20 @@

Many caching schemes are possible with abutting span sorting, because any given pair of polygons, being noninterpenetrating, will sort in the same order throughout a scene. However, in Quake at least, the benefits of caching sort results were outweighed by the additional overhead of maintaining the caching information, and every caching variant we tried actually slowed Quake down.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/67-03.html b/67-03.html index 3cddb11..6b8850e 100644 --- a/67-03.html +++ b/67-03.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Independent Span Sorting

+

Independent Span Sorting

Finally, we come to independent span sorting, the simplest and fastest of the three, and the type the sample code in Listing 67.1 uses. Here, polygons never intersect or touch any other polygons except adjacent polygons with which they form a continuous mesh. This means that when a polygon starts on a scan line, a single 1/z comparison between that polygon and the polygons it overlaps on the screen is guaranteed to produce correct sorting, with no extra calculations or tricky cases to worry about.

@@ -476,16 +479,20 @@ void UpdateWorld() }
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/67-04.html b/67-04.html index 6f8b992..c101a1d 100644 --- a/67-04.html +++ b/67-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

By the same token, Listing 67.1 is quite a bit more complicated than the earlier code. The earlier code’s HSR consisted of a z-sort of objects, followed by the drawing of the objects in back-to-front order, one polygon at a time. Apart from the simple object sorter, all that was needed was backface culling and a polygon rasterizer.

Listing 67.1 replaces this simple pipeline with a three-stage HSR process. After backface culling, the edges of each of the polygons in the scene are added to the global edge list, by way of AddPolygonEdges(). After all edges have been added, the edges are turned into spans by ScanEdges(), with each pixel on the screen being covered by one and only one span (that is, there’s no overdraw). Once all the spans have been generated, they’re drawn by DrawSpans(), and rasterization is complete.

@@ -44,16 +47,20 @@

One more element is required in order for ScanEdges() to work efficiently. Each time a leading or trailing edge occurs, it must be determined whether its surface is nearest (at a larger 1/z value than any currently active surface). In addition, for leading edges, the currently topmost surface must be known, and for trailing edges, it may be necessary to know the currently next-to-topmost surface. The easiest way to accomplish this is with a surface stack; that is, a linked list of all currently active surfaces, starting with the nearest surface and progressing toward the farthest surface, which, as described below, is always the background surface. (The operation of this sort of edge event-based stack was described and illustrated in Chapter 66.) Each leading edge causes its surface to be 1/z-sorted into the surface stack, with a span emitted if necessary. Each trailing edge causes its surface to be removed from the surface stack, again with a span emitted if necessary. As you can see from Listing 67.1, it takes a fair bit of code to implement this, but all that’s really going on is a surface stack driven by edge events.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/67-05.html b/67-05.html index de48c91..f1981a1 100644 --- a/67-05.html +++ b/67-05.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Implementation Notes

+

Implementation Notes

Finally, a few notes on Listing 67.1. First, you’ll notice that although we clip all polygons to the view frustum in worldspace, we nonetheless later clamp them to valid screen coordinates before adding them to the edge list. This catches any cases where arithmetic imprecision results in clipped polygon vertices that are a bit outside the frustum. I’ve only found such imprecision to be significant at very small z distances, so clamping would probably be unnecessary if there were a near clip plane, and might not even be needed in Listing 67.1, because of the slight nudge inward that we give the frustum planes, as described in Chapter 65. However, my experience has consistently been that relying on worldspace or viewspace clipping to produce valid screen coordinates 100 percent of the time leads to sporadic and hard-to-debug errors.

@@ -42,16 +45,20 @@

Lastly, as discussed in Chapter 66, Listing 67.1 uses the gradients for 1/z with respect to changes in screen x and y to calculate 1/z for active surfaces each time a leading edge needs to be sorted into the surface stack. The natural origin for gradient calculations is the center of the screen, which is (x,y) coordinate (0,0) in viewspace. However, when the gradients are calculated in AddPolygonEdges(), the origin value is calculated at the upper-left corner of the screen. This is done so that screen x and y coordinates can be used directly to calculate 1/z, with no need to adjust the coordinates to be relative to the center of the screen. Also, the screen gradients grow more extreme as a polygon is viewed closer to edge-on. In order to keep the gradient calculations from becoming meaningless or generating errors, a small epsilon is applied to backface culling, so that polygons that are very nearly edge-on are culled. This calculation would be more accurate if it were based directly on the viewing angle, rather than on the dot product of a viewing ray to the polygon with the polygon normal, but that would require a square root, and in my experience the epsilon used in Listing 67.1 works fine.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/68-01.html b/68-01.html index 2f9a2a2..41a5ba5 100644 --- a/68-01.html +++ b/68-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 68
Quake’s Lighting Model

@@ -57,20 +60,24 @@

Gouraud shading allows for decent lighting effects with a relatively small amount of calculation and a compact data set that’s a simple extension of the basic polygon model. However, there are several important drawbacks to Gouraud shading, as well.

-

Problems with Gouraud Shading

+

Problems with Gouraud Shading

The quality of Gouraud shading depends heavily on the average size of the polygons being drawn. Linear interpolation is used, so highlights can only occur at vertices, and color gradients are monotonic across the face of each polygon. This can make for bland lighting effects if polygons are large, and makes it difficult to do spotlights and other detailed or dramatic lighting effects. After John brought the initial, primitive Quake engine up using Gouraud shading for lighting, the first thing he tried to improve lighting quality was adding a single vertex and creating new polygons wherever a spotlight was directly overhead a polygon, with the new vertex added directly underneath the light, as shown in Figure 68.1. This produced fairly attractive highlights, but simultaneously made evident several problems.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/68-02.html b/68-02.html index 0bc7cad..0030a3d 100644 --- a/68-02.html +++ b/68-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

A primary problem with Gouraud shading is that it requires the vertices used for world geometry to serve as lighting sample points as well, even though there isn’t necessarily a close relationship between lighting and geometry. This artificial coupling often forces the subdivision of a single polygon into several polygons purely for lighting reasons, as with the spotlights mentioned above; these extra polygons increase the world database size, and the extra transformations and projections that they induce can harm performance considerably.

Similar problems occur with overlapping lights, and with shadows, where additional polygons are required in order to approximate lighting detail well. In particular, good shadow edges need small polygons, because otherwise the gradient between light and dark gets spread across too wide an area. Worse still, the rate of lighting change across a shadow edge can vary considerably as a function of the geometry the edge crosses; wider polygons stretch and diffuse the transition between light and shadow. A related problem is that lighting discontinuities can be very visible at t-junctions (although ultimately we had to add edges to eliminate t-junctions anyway, because otherwise dropouts can occur along polygon edges). These problems can be eased by adding extra edges, but that increases the rasterization load.

@@ -37,7 +40,7 @@


Figure 68.1
  Adding an extra vertex directly beneath a light.

-

Perspective Correctness

+

Perspective Correctness

Another problem is that Gouraud shading isn’t perspective-correct. With Gouraud shading, lighting varies linearly across the face of a polygon, in equal increments per pixel—but unless the polygon is parallel to the screen, the same sort of perspective correction is needed to step lighting across the polygon properly as is required for texture mapping. Lack of perspective correction is not as visibly wrong for lighting as it is for texture mapping, because smooth lighting gradients can tolerate considerably more warping than can the detailed bitmapped images used in texture mapping, but it nonetheless shows up in several ways.

@@ -62,22 +65,26 @@

There are many alternative lighting approaches, most of them higher-quality than Gouraud, starting with Phong shading, in which the surface normal is interpolated across the polygon’s surface, and going all the way up to ray-tracing lighting techniques in which full illumination calculations are performed for all direct and reflected paths from each light source for each pixel. What all these approaches have in common is that they’re slower than Gouraud shading, too slow for our purposes in Quake. For weeks, we kicked around and rejected various possibilities and continued working with Gouraud shading for lack of a better alternative—until the day John came into work and said, “You know, I have an idea....”

-

Decoupling Lighting from Rasterization

+

Decoupling Lighting from Rasterization

John’s idea came to him while was looking at a wall that had been carved into several pieces because of a spotlight, with an ugly lighting glitch due to a t-junction. He thought to himself that if only there were some way to treat it as one surface, it would look better and draw faster—and then he realized that there was a way to do that.

The insight was to split lighting and rasterization into two separate steps. In a normal Gouraud-based rasterizer, there’s first an off-line preprocessing step when the world database is built, during which polygons are added to support additional lighting detail as needed, and lighting values are calculated at the vertices of all polygons. At runtime, the lighting values are modified if dynamic lighting is required, and then the polygons are drawn with Gouraud shading.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/68-03.html b/68-03.html index 434d8a9..049978a 100644 --- a/68-03.html +++ b/68-03.html @@ -1,5 +1,4 @@ - + @@ -19,22 +18,26 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Quake’s approach, which I’ll call surface-based lighting, preprocesses differently, and adds an extra rendering step. During off-line preprocessing, a grid, called a light map, is calculated for each polygon in the world, with a lighting value every 16 texels horizontally and vertically. This lighting is done by casting light from all the nearby lights in the world to each of the grid points on the polygon, and summing the results for each grid point. The Quake preprocessor filters the values, so shadow edges don’t have a stair-step appearance (a technique suggested by Billy Zelsnack); additional preprocessing could be done, for example Phong shading to make surfaces appear smoothly curved. Then, at runtime, the polygon’s texture is tiled into a buffer, with each texel lit according to the weighted average intensities of the four nearest light map points, as shown in Figure 68.3. If dynamic lighting is needed, the light map is modified accordingly before the buffer, which I’ll call a surface, is built. Then the polygon is drawn with perspective texture mapping, with the surface serving as the input texture, and with no lighting performed during the texture mapping.

So what does surface-based lighting buy us? First and foremost, it provides consistent, perspective-correct lighting, eliminating all rotational, viewing, and clipping variance, because lighting is done in surface space rather than in screen space. By lighting in surface space, we bind the lighting to the texels in an invariant way, and then the lighting gets a free ride through the perspective texture mapper and ends up perfectly matched to the texels. Surface-based lighting also supports good, although not perfect, detail for overlapping lights and shadows. The 16-texel grid has a resolution of two feet in the Quake frame of reference, and this relatively fine resolution, together with the filtering performed when the light map is built, is sufficient to support complex shadows with smoothly fading edges. Additionally, surface-based lighting eliminates lighting glitches at t-junctions, because lighting is unrelated to vertices. In short, surface-based lighting meets all of Quake’s visual quality goals, which leaves only one question: How does it perform?

-

Size and Speed

+

Size and Speed

As it turns out, the raw speed of surface-based lighting is pretty good. Although an extra step is required to build the surface, moving lighting and tiling into a separate loop from texture mapping allows each of the two loops to be optimized very effectively, with almost all variables kept in registers. The surface-building inner loop is particularly efficient, because it consists of nothing more than interpolating intensity, combining it with a texel and using the result to look up a lit texel color, and storing the results with a dword write every four texels. In assembly language, we got this code down to 2.25 cycles per lit texel in Quake. Similarly, the texture-mapping inner loop, which overlaps an FDIV for floating-point perspective correction with integer pixel drawing in 16-pixel bursts, has been squeezed down to 7.5 cycles per pixel on a Pentium, so the combined inner loop times for building and drawing a surface is roughly in the neighborhood of 10 cycles per pixel. It’s certainly possible to write a Gouraud-shaded perspective-correct texture mapper that’s somewhat faster than 10 cycles, but 10 cycles/pixel is fast enough to do 40 frames/second at 640x400 on a Pentium/100, so the cycle counts of surface-based lighting are acceptable. It’s worth noting that it’s possible to write a one-pass texture mapper that does approximately perspective-correct lighting. However, I have yet to hear of or devise such an inner loop that isn’t complicated and full of special cases, which makes it hard to optimize; worse, this approach doesn’t work well with the procedural and post-processing techniques I’ll discuss shortly.

@@ -51,16 +54,20 @@

With surface rebuilding needed only rarely, thanks to surface caching, Quake’s rasterization speed is generally the speed of the unlit, perspective-correct texture-mapping inner loop, which suffers from more cache misses than Gouraud-shaded, tiled texture mapping, but doesn’t have the overhead of Gouraud shading, and allows the use of larger polygons. In the worst case, where everything in a frame is a new surface, the speed of the surface-caching approach is somewhat slower than Gouraud shading, but generally surface caching provides equal or better performance, so once surface caching was implemented in Quake, performance was no longer a problem—but size became a concern.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/68-04.html b/68-04.html index 3a4a792..bd15de8 100644 --- a/68-04.html +++ b/68-04.html @@ -1,5 +1,4 @@ - + @@ -19,20 +18,24 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The amount of memory required for surface caching looked forbidding at first. Surfaces are large relative to texture tiles, because every texel of every surface is unique. Also, a surface can contain many texels relative to the number of pixels actually drawn on the screen, because due to perspective foreshortening, distant polygons have only a few pixels relative to the surface size in texels. Surfaces associated with partly hidden polygons must be fully built, even though only part of the polygon is visible, and if polygons are drawn back to front with overdraw, some polygons won’t even be visible, but will still require surface building and caching. What all this meant was that the surface cache initially looked to be very large, on the order of several megabytes, even at 320x200—too much for a game intended to run on an 8 MB machine.

-

Mipmapping To The Rescue

+

Mipmapping To The Rescue

Two factors combined to solve this problem. First, polygons are drawn through an edge list with no overdraw, as I discussed a few chapters back, so no surface is ever built unless at least part of it is visible. Second, surfaces are built at four mipmap levels, depending on distance, with each mipmap level having one-quarter as many texels as the preceding level, as shown in Figure 68.4.

@@ -45,22 +48,26 @@

Also, mipmapping is done on a per-surface basis; the mipmap level for a whole surface is selected based on the distance from the viewer of the nearest vertex. This led us to limit surface size to a maximum of 256x256. Otherwise, surfaces such as floors would extend for thousands of texels, all at the mipmap level of the nearest vertex, and would require huge amounts of surface cache space while displaying a great deal of aliasing in distant regions due to a high texel:pixel ratio.

-

Two Final Notes on Surface Caching

+

Two Final Notes on Surface Caching

Dynamic lighting has a significant impact on the performance of surface caching, because whenever the lighting on a surface changes, the surface has to be rebuilt. In the worst case, where the lighting changes on every visible surface, the surface cache provides no benefit, and rendering runs at the combined speed of surface building and texture mapping. This worst-case slowdown is tolerable but certainly noticeable, so it’s best to design games that use surface caching so only some of the surfaces change lighting at any one time. If necessary, you could alternate surface relighting so that half of the surfaces change on even frames, and half on odd frames, but large-scale, constant relighting is not surface caching’s strongest suit.

Finally, Quake barely begins to tap surface caching’s potential. All sorts of procedural texturing and post-processing effects are possible. If a wall is shot, a sprite of pockmarks could be attached to the wall’s data structure, and the sprite could be drawn into the surface each time the surface is rebuilt. The same could be done for splatters, or graffiti, with translucency easily supported. These effects would then be cached and drawn as part of the surface, so the performance cost would be much less than effects done by on-screen overdraw every frame. Basically, the surface is a handy repository for all sorts of effects, because multiple techniques can be composited, because it caches the results for reuse without rebuilding, and because the texels constructed in a surface are automatically drawn in perspective.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/69-01.html b/69-01.html index 203811d..448b544 100644 --- a/69-01.html +++ b/69-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 69
Surface Caching and Quake’s Triangle Models

@@ -61,16 +64,20 @@

Does my recent experience indicate that as the PC market moves to hardware, there’s no choice but to move to Gouraud shading, despite the quality issues? Not at all. First of all, surface caching does still work well, just not as relatively well compared to Gouraud shading as is the case in software. Second, there are at least two alternatives that preserve the advantages of surface caching without many of the disadvantages noted above.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/69-02.html b/69-02.html index ca36aa4..c5ed717 100644 --- a/69-02.html +++ b/69-02.html @@ -1,5 +1,4 @@ - + @@ -19,22 +18,26 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

Letting the Graphics Card Build the Textures

+

Letting the Graphics Card Build the Textures

One obvious solution is to have the accelerator card build the textures, rather than having the CPU build and then download them. This eliminates downloading completely, and lets the accelerator, which should be faster at such things, do the texel manipulation. Whether this is actually faster depends on whether the CPU or the accelerator is doing more of the work overall, but it eliminates download time, which is a big help. This approach retains the ability to composite other effects, such as splatters and dents, onto surfaces, but by the same token retains the high memory requirements and dynamic lighting performance impact of the surface cache. It also requires that the 3-D API and accelerator being used allow drawing into a texture, which is not universally true. Neither do all APIs or accelerators allow applications enough control over the texture heap so that an efficient surface cache can be implemented, a point that favors non-caching approaches. (A similar option that wasn’t open to us due to time limitations is downloading 8-bpp surfaces and having the accelerator expand them to 16-bpp surfaces as it stores them in texture memory. Better yet, some accelerators support 8-bpp palettized hardware textures that are expanded to 16-bpp on the fly during texturing.)

-

The Light Map as Alpha Texture

+

The Light Map as Alpha Texture

Another appealing non-caching approach is doing unlit texture-mapping in one pass, then lighting from the light map as a second pass, using the light map as an alpha texture. In other words, the textured polygon is drawn first, with no lighting, then the light map is textured on top of the polygon, with the light map intensity used as an alpha value to determine how brightly to light each texel. The hardware’s texture-mapping circuitry is used for both passes, so the lighting comes out perspective-correct and consistent under all viewing conditions, just as with the surface cache. The lighting polygons don’t even have to match the texture polygons, so they can represent dynamically changing lighting.

@@ -46,22 +49,26 @@

Most of the last group of chapters in this book discuss how Quake works. If you look closely, though, you’ll see that almost all of the information is about drawing the world—the static walls, floors, ceilings, and such. There are several reasons for this, in particular that it’s hard to get a world renderer working well, and that the world is the base on which everything else is drawn. However, moving entities, such as monsters, are essential to a useful game engine. Traditionally, these have been done with sprites, but when we set out to build Quake, we knew that it was time to move on to polygon-based models. (In the case of Quake, the models are composed of triangles.) We didn’t know exactly how we were going to make the drawing of these models fast enough, though, and went through quite a bit of experimentation and learning in the process of doing so. For the rest of this chapter I’ll discuss some interesting aspects of our triangle-model architecture, and present code for one useful approach for the rapid drawing of triangle models.

-

Drawing Triangle Models Fast

+

Drawing Triangle Models Fast

We would have liked one rendering model, and hence one graphics pipeline, for all drawing in Quake; this would have simplified the code and tools, and would have made it much easier to focus our optimization efforts. However, when we tried adding polygon models to Quake’s global edge table, edge processing slowed down unacceptably. This isn’t that surprising, because the edge table was designed to handle 200 to 300 large polygons, not the 2,000 to 3,000 tiny triangles that a dozen triangle models in a scene can add. Restructuring the edge list to use trees rather than linked lists would have helped with the larger data sets, but the basic problem is that the edge table requires a considerable amount of overhead per edge per scan line, and triangle models have too few pixels per edge to justify that overhead. Also, the much larger edge table generated by adding triangle models doesn’t fit well in the CPU cache.

Consequently, we implemented a separate drawing pipeline for triangle models, as shown in Figure 69.1. Unlike the world pipeline, the triangle-model pipeline is in most respects a traditional one, with a few exceptions, noted below. The entire world is drawn first, and then the triangle models are drawn, using z-buffering for proper visibility. For each triangle model, all vertices are transformed and projected first, and then each triangle is drawn separately.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/69-03.html b/69-03.html index 595161a..0d6e7a8 100644 --- a/69-03.html +++ b/69-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Triangle models are stored quite differently from the world itself. Each model consists of front and back skins stretched around a triangle mesh, and contains a full set of vertex coordinates for each animation frame, so animation is performed by simply using the correct set of coordinates for the desired frame. No interpolation, morphing, or other runtime vertex calculations are performed.

Early on, we decided to allow lower drawing quality for triangle models than for the world, in the interests of speed. For example, the triangles in the models are small, and usually distant—and generally part of a quickly moving monster that’s trying its best to do you in—so the quality benefits of perspective texture mapping would add little value. Consequently, we chose to draw the triangles with affine texture mapping, avoiding the work required for perspective. Mind you, the models are perspective-correct at the vertices; it’s just the pixels between the vertices that suffer slight warping.

@@ -37,7 +40,7 @@


Figure 69.1
  Quake’s triangle-model drawing pipeline.

-

Trading Subpixel Precision for Speed

+

Trading Subpixel Precision for Speed

Another sacrifice at the altar of performance was subpixel precision. Before each triangle is drawn, we snap its vertices to the nearest integer screen coordinates, rather than doing the extra calculations to handle fractional vertex coordinates. This causes some jumping of triangle edges, but again, is not a problem in normal gameplay, especially for the animation of figures in continuous motion.

@@ -45,7 +48,7 @@

Finally, we decided to Gouraud-shade the triangle models, because this makes them look considerably more 3-D. However, we can’t afford to calculate where all the relevant light sources for each model are in each frame, or even which is the primary light source. Instead, we select each model’s lighting level based on how brightly the floor point it was standing on is lit, and use that lighting level for both ambient lighting (so all parts of the model have some illumination) and Gouraud shading—but the lighting vector for Gouraud shading is a fixed vector, so the model is always lit from the same direction. Somewhat surprisingly, in practice this looks considerably better than pure ambient lighting.

-

An Idea that Didn’t Work

+

An Idea that Didn’t Work

As we implemented triangle models, we tried several ideas that didn’t work out. One that’s notable because it seems so appealing is caching a model’s image from one frame and reusing it in the next frame as a sprite. Our thinking was that clipping, transforming, projecting, and drawing a several-hundred-triangle model was going to be a lot more expensive than drawing a sprite, too expensive to allow very many models to be visible at once. We wanted to be able to display at least a dozen simultaneous models, so the idea was that for all but the closest models, we’d draw into a sprite, then reuse that sprite at the model’s new locations for the next two or three frames, amortizing the 3-D drawing cost over several frames and boosting overall model-drawing performance. The rendering wouldn’t be exactly right when the sprite was reused, because the view of the model would change from frame to frame as the viewer and model moved, but it didn’t seem likely that that slight inaccuracy would be noticeable for any but the nearest and largest models.

@@ -53,22 +56,26 @@

The sprite architecture also introduced considerable code complexity, increased memory footprint because of the need to cache the sprites, and made it difficult to get hidden surfaces exactly right because sprites are unavoidably 2-D. The performance of drawing the sprites dropped sharply as models got closer, and that’s also where the sprites looked worse when they were reused, limiting sprites to use at a considerable distance. All these problems could have been worked out reasonably well if necessary, but the sprite architecture just had the feeling of being fundamentally not the right approach, so we tried thinking along different lines.

-

An Idea that Did Work

+

An Idea that Did Work

John Carmack had the notion that it was just way too much effort per pixel to do all the work of scanning out the tiny triangles in distant models. After all, distant models are just indistinct blobs of pixels, suffering heavily from effects such as texture aliasing and pixel quantization, he reasoned, so it should work just as well if we could come up with another way of drawing blobs of approximately equal quality. The trick was to come up with such an alternative approach. We tossed around half-formed ideas like flood-filling the model’s image within its silhouette, or encoding the model as a set of deltas, picking a visible seed point, and working around the visible side of the model according to the deltas. The first approach that seemed practical enough to try was drawing the pixel at each vertex replicated to form a 2x2 box, with all the vertices together forming the approximate shape of the model. Sometimes this worked quite well, but there were gaps where the triangles were large, and the quality was very erratic. However, it did point the way to something that in the end did the trick.

One morning I came in to the office to find that overnight (and well into the morning), John had designed and implemented a technique I’ll call subdivision rasterization. This technique scans out approximately the right pixels for each triangle, with almost no overhead, as follows. First, all vertices in the model are drawn. Ideally, only the vertices on the visible side of the model would be drawn, but determining which vertices those are would take time, and the occasional error from a visible back vertex is lost in the noise.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/69-04.html b/69-04.html index 2c89c61..af70b7d 100644 --- a/69-04.html +++ b/69-04.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Once the vertices are drawn, the triangles are processed one at a time. Each triangle that makes it through backface culling is then drawn with recursive subdivision. If any of the triangle’s sides is more than one pixel long in either x or y—that is, if the triangle contains any pixels that aren’t at vertices—then that side is split in half as nearly as possible at given integer coordinates, and a new vertex is created at the split, with texture and screen coordinates that are halfway between those of the vertices at the endpoints. (The same splitting could be done for lighting, but we found that for small triangles—the sort that subdivision works well on—it was adequate to flat-shade each triangle at the light level of the first vertex, so we didn’t bother with Gouraud shading.) The halfway values can be calculated very quickly with shifts. This vertex is drawn, and then each of the two resulting triangles is then processed recursively in the same way, as shown in Figure 69.2. There are some additional details, such as the fill rule that ensures that each pixel is drawn only once (except for backside vertices, as noted above), but basically subdivision rasterization boils down to taking a triangle, splitting a side that has at least one undrawn pixel and drawing the vertex at the split, and repeating the process for each of the two new triangles. The code to do this, shown in Listing 69.1, is very simple and easily optimized, especially by comparison with a generalized triangle rasterizer.

Subdivision rasterization introduces considerably more error than affine texture mapping, and doesn’t draw exactly the right triangle shape, but the difference is very hard to detect for triangles that contain only a few pixels. We found that the point at which the difference between the two rasterizers becomes noticeable was surprisingly close: 30 or 40 feet for the Ogres, and about 12 feet for the Zombies. This means that most of the triangle models that are visible in a typical Quake scene are drawn with subdivision rasterization, not affine texture mapping.

@@ -155,7 +158,7 @@ D_PolysetRecursiveTriangle (lp3, new, lp2);


Figure 69.2
  One recursive subdivision triangle-drawing step.

-

More Ideas that Might Work

+

More Ideas that Might Work

Useful as subdivision rasterization proved to be, we by no means think that we’ve maxed out triangle-model drawing, if only because we spent far less design and development time on subdivision than on the affine rasterizer, so it’s likely that there’s quite a bit more performance to be found for drawing small triangles. For example, it could be faster to precalculate drawing masks or even precompile drawing code for all possible small triangles (say, up to 4x4 or 5x5), and the memory footprint looks reasonable. (It’s worth noting that both precalculated drawing and subdivision rasterization are only possible because we snap to integer coordinates; none of this stuff works with fixed-point vertices.)

@@ -163,16 +166,20 @@ D_PolysetRecursiveTriangle (lp3, new, lp2);

As with so many aspects of 3-D, there is no one best approach to drawing triangle models, and no such thing as the fastest code. In a way, that’s frustrating, but the truth is, it’s these nearly infinite possibilities that make 3-D so interesting; not only is it an endless, varied challenge, but there’s almost always a better solution waiting to be found.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/70-01.html b/70-01.html index f9c31c2..346facf 100644 --- a/70-01.html +++ b/70-01.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Chapter 70
Quake: A Post-Mortem and a Glimpse into the Future

@@ -55,16 +58,20 @@

The BSP tree is built using the polygon that splits the fewest of the polygons in the current node’s subspace as the heuristic for choosing splitters, which is not an optimal solution—but an optimal solution is NP-complete, and our heuristic adds only 10% to 15% more polygons to the level as a result of BSP splits. Polygons are not split all the way into leaves; rather, they are placed on the nodes with which they are coplanar (one set on the front and one on the back, which has the advantage of letting us reuse the BSP-walking dot product for backface culling as well), thereby reducing splitting considerably, because polygons are split only by parent nodes, not by child nodes (as would be necessary if polygons were split into leaves). Eliminating polygon splits, thus reducing the total number of polygons per level, not only shrinks Quake’s memory footprint, but also reduces the number of polygons that need to be processed by the 3-D pipeline, producing a speedup of about 10% in Quake’s overall performance.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/70-02.html b/70-02.html index e086422..fe9e12d 100644 --- a/70-02.html +++ b/70-02.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Getting proper front-to-back drawing order is a little more complicated with polygons on nodes. As we walk the BSP tree front-to-back, in each leaf we mark the polygons that are at least partially in that leaf, and then after we’ve recursed and processed everything in front of a node, we then process all the marked polygons on that node, after which we recurse to process the polygons behind the node. So putting the polygons on the nodes saves memory and improves performance significantly, but loses the simple approach of simply recursing the tree and processing the polygons in each leaf as we come to it, in favor of recursing and marking in front of a node, processing marked polygons on the node, then recursing behind the node.

After the BSP is built, the outer surfaces of the level, which no one can ever see (because levels are sealed spaces), are removed, so the interior of the level, containing all the empty space through which a player can move, is completely surrounded by a solid region. This eliminates a great many irrelevant polygons, and reduces the complexity of the next step, calculating the potentially visible set.

@@ -48,16 +51,20 @@

The final preprocessing step is light map generation. Each light is traced out into the world to see what polygons it strikes, and the cumulative effect of all lights on each surface is stored as a light map, a sampling of light values on a 16-texel grid. In Quake 2, radiosity lighting—a considerably more expensive process, but one that produces highly realistic lighting—is performed, but I’ll save that for later.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/70-03.html b/70-03.html index 0af69e9..43b790d 100644 --- a/70-03.html +++ b/70-03.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Passages: The Last-Minute Change that Didn’t Happen

Earlier, I mentioned that we almost changed 3-D engines again in the last month of Quake’s development. Here’s what happened: One of the alternatives to the PVS is the use of portals, where the focus is on the places where polygons don’t exist along leaf faces, rather than the more usual focus on the polygons themselves. These “empty” places are themselves polygons, called portals, that describe all the places that visibility can pass from one leaf to another. Portals are used by the PVS generator to determine visibility, and are used in other 3-D engines as the primary mechanism for determining leaf or sector visibility. For example, portals can be projected to screenspace, then used as a 2-D clipping region to restrict drawing of more distant polygons to only those that are visible through the portal. Or, as in Quake’s preprocessor, visibility boundary planes can be constructed from one portal to the next, and 3-D clipping to those planes can be used to determine visible polygons or leaves. Used either way, portals can support more changeable worlds than the PVS, because, unlike the PVS, the portals themselves can easily be changed on the fly.

@@ -58,16 +61,20 @@ -


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/70-04.html b/70-04.html index d9595b4..44ed7d8 100644 --- a/70-04.html +++ b/70-04.html @@ -1,5 +1,4 @@ - + @@ -19,30 +18,34 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

The edge list is an atypical technology for John; it’s an extra stage in the engine, it’s complex, and it doesn’t scale well. A Quake level might have a maximum of 500 potentially drawable polygons that get placed into the edge list, and that runs fine, but if you were to try to put 5,000 polygons into the edge list, it would quickly bog down due to edge sorting, link following, and dataset size. Different data structures (like using a tree to store the edges rather than a linear linked list) would help to some degree, but basically the edge list has a relatively small window of applicability; it was appropriate technology for the degree of complexity possible in a Pentium-based game (and even then, only with the reduction in polygons made possible by the PVS), but will probably be poorly suited to more complex scenes. It served well in the Quake engine, but remains an inelegant solution, and, in the end, it feels like there’s something better we didn’t hit on. However, as John says, “I’m pragmatic above all else”—and the edge list did the job.

Rasterization

Once the visible spans are scanned out of the edge list, they must still be drawn, with perspective-correct texture mapping and lighting. This involves hundreds of lines of heavily optimized assembly language, but is fundamentally pretty simple. In order to draw the spans for a given surface, the screenspace equations for 1/z, s/z, and t/z (where s and t are the texture coordinates and z is distance) are calculated for the surface. Then for each span, these values are calculated for the points at each end of the span, the reciprocal of 1/z is calculated with a divide, and s and t are then calculated as (s/z)*z and (t/z)*z. If the span is longer than 16 pixels, s and t are likewise calculated every 16 pixels along the span. Then each stretch of up to 16 pixels is drawn by linearly interpolating between these correctly calculated points. This introduces some slight error, but this is almost never visible, and even then is only a small ripple, well worth the performance improvement gained by doing the perspective-correct math only once every 16 pixels. To speed things up a little more, the FDIV to calculate the reciprocal of 1/z is overlapped with drawing 16 pixels, taking advantage of the Pentium’s ability to perform floating-point in parallel with integer instructions, so the FDIV effectively takes only one cycle.

-

Lighting

+

Lighting

Lighting is less simple to explain. The traditional way of doing polygon lighting is to calculate the correct light at the vertices and linearly interpolate between those points (Gouraud shading), but this has several disadvantages; in particular, it makes it hard to get detailed lighting without creating a lot of extra polygons, the lighting isn’t perspective correct, and the lighting varies with viewing angle for polygons other than triangles. To address these problems, Quake uses surface-based lighting instead. In this approach, when it’s time to draw a surface (a world polygon), that polygon’s texture is tiled into a memory buffer. At the same time, the texture is lit according to the surface’s light map, as calculated during preprocessing. Lighting values are linearly interpolated between the light map’s 16-texel grid points, so the lighting effects are smooth, but slightly blurry. Then, the polygon is drawn to the screen using the perspective-correct texture mapping described above, with the prelit surface buffer being the source texture, rather than the original texture tile. No additional lighting is performed during texture mapping; all lighting is done when the surface buffer is created.

Certainly it takes longer to build a surface buffer and then texture map from it than it does to do lighting and texture mapping in a single pass. However, surface buffers are cached for reuse, so only the texture mapping stage is usually needed. Quake surfaces tend to be big, so texture mapping is slowed by cache misses; however, the Quake approach doesn’t need to interpolate lighting on a pixel-by-pixel basis, which helps speed things up, and it doesn’t require additional polygons to provide sophisticated lighting. On balance, the performance of surface-based drawing is roughly comparable to tiled, Gouraud-shaded texture mapping—and it looks much better, being perspective correct, rotationally invariant, and highly detailed. Surface-based drawing also has the potential to support some interesting effects, because anything that can be drawn into the surface buffer can be cached as well, and is automatically drawn in correct perspective. For instance, paint splattered on a wall could be handled by drawing the splatter image as a sprite into the appropriate surface buffer, so that drawing the surface would draw the splatter as well.

-

Dynamic Lighting

+

Dynamic Lighting

Here we come to a feature added to Quake after last year’s Computer Game Developer’s Conference (CGDC). At that time, Quake did not support dynamic lighting; that is, explosions and such didn’t produce temporary lighting effects. We hadn’t thought dynamic lighting would add enough to the game to be worth the trouble; however, at CGDC Billy Zelsnack showed us a demo of his latest 3-D engine, which was far from finished at the time, but did have impressive dynamic lighting effects. This caused us to move dynamic lighting up the priority list, and when I got back to id, I spent several days making the surface-building code as fast as possible (winding up at 2.25 cycles per texel in the inner loop) in anticipation of adding dynamic lighting, which would of course cause dynamically lit surfaces to constantly be rebuilt as the lighting changed. (A significant drawback of dynamic lighting is that it makes surface caching worthless for dynamically lit surfaces, but if most of the surfaces in a scene are not dynamically lit at any one time, it works out fine.) There things stayed for several weeks, while more critical work was done, and it was uncertain whether dynamic lighting would, in fact, make it into Quake.

@@ -52,16 +55,20 @@

It’s well worth pointing out that because Quake’s lighting is perspective correct and independent of vertices, and because the rasterizer is both subpixel and subtexel correct, Quake worlds are visually very solid and stable. This was an important design goal from the start, both as a point of technical pride and because it greatly improves the player’s sense of immersion.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/70-05.html b/70-05.html index 1ef11e0..1e78422 100644 --- a/70-05.html +++ b/70-05.html @@ -1,5 +1,4 @@ - + @@ -19,22 +18,26 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Entities

So far, all we’ve drawn is the static, unchanging (apart from dynamic lighting) world. That’s an important foundation, but it’s certainly not a game; now we need to add moving objects. These objects fall into four very different categories: BSP models, polygon models, sprites, and particles.

-

BSP Models

+

BSP Models

BSP models are just like the world, except that they can move. Examples include doors, moving bridges, and health and ammo boxes. The way these are rendered is by clipping their polygons into the world BSP tree, so each polygon fragment is in only one leaf. Then these fragments are added to the edge list, just like world polygons, and scanned out, along with the rest of the world, when the edge list is processed. The only trick here is front-to-back ordering. Each BSP model polygon fragment is given the BSP sorting order of the leaf in which it resides, allowing it to sort properly versus the world polygons. If two or more polygons from different BSP models are in the same leaf, however, BSP ordering is no longer useful, so we then sort those polygons by 1/z, calculated from the polygons’ plane equations.

@@ -42,7 +45,7 @@

BSP models take some extra time because of the cost of clipping them into the world BSP tree, but render just as fast as the rest of the world, again with no overdraw, so closed doors, for example, block drawing of whatever’s on the other side (although it’s still necessary to transform, project, and add to the edge list the polygons the door occludes, because they’re still in the PVS—they’re potentially visible if the door opens). This makes BSP models most suitable for fairly simple structures, such as boxes, which have relatively few polygons to clip, and cause relatively few edges to be added to the edge list.

-

Polygon Models and Z-Buffering

+

Polygon Models and Z-Buffering

Polygon models, such as monsters, weapons, and projectiles, consist of a triangle mesh with front and back skins stretched over the model. For speed, the triangles are drawn with affine texture mapping; the triangles are small enough, and the models are generally distant enough, that affine distortion isn’t visible. (However, it is visible on the player’s weapon; this caused a lot of extra work for the artists, and we will probably implement a perspective-correct polygon-model rasterizer in Quake 2 for this specific purpose.) The triangles are also Gouraud shaded; interestingly, the light vector used to shade the models is always from the same direction, and has no relation to any actual lights in the world (although it does vary in intensity, along with the model’s ambient lighting, to match the brightness of the spot the player is standing above in the world). Even this highly inaccurate lighting works well, though; the Gouraud shading makes models look much more three-dimensional, and varying the lighting in even so crude a way allows hiding in shadows and illumination by explosions and muzzle flashes.

@@ -54,16 +57,20 @@

Supporting scenes with a dozen or more models of 300 to 500 polygons each was a major performance challenge in Quake, and the polygon-model drawing code was being optimized right up until the last week before it shipped. One help in allowing more models per scene was the PVS; we only drew those models that were in the PVS, meaning that levels could have a hundred or more models without requiring a lot of work to eliminate most of those that were occluded. (Note that this is not unique to the PVS; whatever high-level culling scheme we had ended up using for world polygons would have provided the same benefit for polygon models.) Also, model bounding boxes were used to trivially clip those that weren’t in the view pyramid, and to identify those that were unclipped, so they could be sent through a special fast path. The biggest breakthrough, though, was a very different sort of rasterizer that John came up with for relatively distant models.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/70-06.html b/70-06.html index 43cf894..a406050 100644 --- a/70-06.html +++ b/70-06.html @@ -1,5 +1,4 @@ - + @@ -19,28 +18,32 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

The Subdivision Rasterizer

+

The Subdivision Rasterizer

This rasterizer, which we call the subdivision rasterizer, first draws all the vertices in the model. Then it takes each front-facing triangle, and determines if it has a side that’s at least two pixels long. If it does, we split that side into two pieces at the pixel nearest to the middle (using adds and shifts to average the endpoints of that side), draw the vertex at the split point, and process each of the two split triangles recursively, until we get down to triangles that have only one-pixel sides and hence have nothing left to draw. This approach is hideously slow and quite ugly (due to inaccuracies from integer quantization) for 100-pixel triangles—but it’s very fast for, say, five-pixel triangles, and is indistinguishable from more accurate rasterization when a model is 25 or 50 feet away. Better yet, the subdivider is ridiculously simple—a few dozen lines of code, far simpler than the affine rasterizer—and was implemented in an evening, immediately making the drawing of distant models about three times as fast, a very good return for a bit of conceptual work. The affine rasterizer got fairly close to the same performance with further optimization—in the range of 10% to 50% slower—but that took weeks of difficult programming.

We switch between the two rasterizers based on the model’s distance and average triangle size, and in almost any scene, most models are far enough away so subdivision rasterization is used. There are undoubtedly faster ways yet to rasterize distant models adequately well, but the subdivider was clearly a win, and is a good example of how thinking in a radically different direction can pay off handsomely.

-

Sprites

+

Sprites

We had hoped to be able to eliminate sprites completely, making Quake 100% 3-D, but sprites—although sometimes very visibly 2-D—were used for a few purposes, most noticeably the cores of explosions. As of CGDC last year, explosions consisted of an exploding spray of particles (discussed below), but there just wasn’t enough visual punch with that representation; adding a series of sprites animating an explosion did the trick. (In hindsight, we probably should have made the explosions polygon models rather than sprites; it would have looked about as good, and the few sprites we used didn’t justify the considerable amount of code and programming time required to support them.) Drawing a sprite is similar to drawing a normal polygon, complete with perspective correction, although of course the inner loop must detect and skip over transparent pixels, and must also perform z-buffering.

-

Particles

+

Particles

The last drawing entity type is particles. Each particle is a solid-colored rectangle, scaled by distance from the viewer and drawn with z-buffering. There can be up to 2,000 particles in a scene, and they are used for rocket trails, explosions, and the like. In one sense, particles are very primitive technology, but they allow effects that would be extremely difficult to do well with the other types of entities, and they work well in tandem with other entities, as, for example, providing a trail of fire behind a polygon-model lava ball that flies into the air, or generating an expanding cloud around a sprite explosion core.

@@ -48,7 +51,7 @@

Since shipping Quake in the summer of 1996, we’ve extended it in several ways: We’ve worked with Rendition to port it to the Verite accelerator chip, we’ve ported it to OpenGL, we’ve ported it to Win32, we’ve done QuakeWorld, and we’ve added features for Quake 2. I’ll discuss each of these briefly.

-

Verite Quake

+

Verite Quake

Verite Quake (VQuake) was the first hardware-accelerated version of Quake. It looks extremely good, due to bilinear texture filtering, which eliminates most pixel aliasing, and because it provides good performance at higher resolutions such as 512x384 and 640x480. Implementing VQuake proved to be an interesting task, for two reasons: The Verite chip’s fill rate was marginal for Quake’s needs, and Verite contains a programmable RISC chip, enabling more sophisticated processing than most 3-D accelerators. The need to squeeze as much performance as possible out of Verite ruled out the use of a standard API such as Direct 3D or OpenGL; instead, VQuake uses Rendition’s proprietary API, Speedy3D, with the addition of some special calls and custom Verite code.

@@ -58,16 +61,20 @@

An alternative to surface caching would have been to do two passes across each span, one tiling the texture, and the other doing an alpha blend using the light map as a texture, to light the texture (two-pass alpha lighting). This approach produces exactly the same results as the surface cache, without requiring downloading and caching of large surfaces, and has the advantage of very level performance. However, this approach requires at least twice the fill rate of the surface cache approach, and Verite didn’t have enough fill rate for that at higher resolutions. It’s also worth noting that two-pass alpha lighting doesn’t have the same potential for procedural texturing that surface caching does. In fact, given MMX and ever-faster CPUs, and the ability of the CPU and the accelerator to process in parallel, it will become increasingly tempting to use the CPU to build surfaces with procedural texturing such as bump mapping, shimmers, and warps; this sort of procedural texturing has the potential to give accelerated games highly distinctive visuals. So the choice between surface caching and two-pass alpha lighting for hardware accelerators depends on a game’s needs, and it seems most likely that the two approaches will be mixed together, with surface caching used for special surfaces, and two-pass alpha lighting used for most drawing.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/70-07.html b/70-07.html index 501b536..d1a4210 100644 --- a/70-07.html +++ b/70-07.html @@ -1,5 +1,4 @@ - + @@ -19,18 +18,22 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


- -

GLQuake

+

GLQuake

The second (and, according to current plans, last) port of Quake to a hardware accelerator was an OpenGL version, GLQuake, a native Win32 application. I have no intention of getting into the 3-D API wars currently raging; the observation I want to make here is that GLQuake uses two-pass alpha lighting, and runs very well on fast chips such as the 3Dfx, but rather slowly on most of the current group of accelerators. The accelerators coming out this year should all run GLQuake fine, however. It’s also worth noting that we’ll be using two-pass alpha lighting in the N64 port of Quake; in fact, it looks like the N64’s hardware is capable of performing both texture-tiling and alpha-lighting in a single pass, which is pretty much an ideal hardware-acceleration architecture: It’s as good looking and generally faster than surface caching, without the need to build, download, and cache surfaces, and much better looking and about as fast as Gouraud shading. We hope to see similar capabilities implemented in PC accelerators and exposed by 3-D APIs in the near future.

@@ -46,26 +49,30 @@

Both alpha-blending and z-buffering are relatively new to PC games, but are standard equipment on accelerators, and it’s a lot of fun seeing what sorts of previously very difficult effects can now be up and working in a matter of hours.

-

WinQuake

+

WinQuake

I’m not going to spend much time on the Win32 port of Quake; most of what I learned doing this consists of tedious details that are doubtless well covered elsewhere, and frankly it wasn’t a particularly interesting task and was harder than I expected, and I’m pretty much tired of the whole thing. However, I will say that Win32 is clearly the future, especially now that NT is coming on strong, and like it or not, you had best learn to write games for Win32. Also, Internet gaming is becoming ever more important, and Win32’s built-in TCP/IP support is a big advantage over DOS; that alone was enough to convince us we had to port Quake. As a last comment, I’d say that it is nice to have Windows take care of device configuration and interfacing—now if only we could get manufacturers to write drivers for those devices that actually worked reliably! This will come as no surprise to veteran Windows programmers, who have suffered through years of buggy 2-D Windows drivers, but if you’re new to Windows programming, be prepared to run into and learn to work around—or at least document in your readme files—driver bugs on a regular basis.

Still, when you get down to it, the future of gaming is a networked Win32 world, and that’s that, so if you haven’t already moved to Win32, I’d say it’s time.

-

QuakeWorld

+

QuakeWorld

QuakeWorld is a native Win32 multiplayer-only version of Quake, and was done as a learning experience; it is not a commercial product, but is freely distributed on the Internet. The idea behind it was to try to improve the multiplayer experience, especially for people linked by modem, by reducing actual and perceived latency. Before I discuss QuakeWorld, however, I should discuss the evolution of Quake’s multiplayer code.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/70-08.html b/70-08.html index 3426a23..e9e397e 100644 --- a/70-08.html +++ b/70-08.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

From the beginning, Quake was conceived as a client-server app, specifically so that it would be possible to have persistent servers always running on the Internet, independent of whether anyone was playing on them at any particular time, as a step toward the long-term goal of persistent worlds. Also, client-server architectures tend to be more flexible and robust than peer-to-peer, and it is much easier to have players come and go at will with client-server. Quake is client-server from the ground up, and even in single-player mode, messages are passed through buffers between the client code and the server code; it’s quite likely that the client and server would have been two processes, in fact, were it not for the need to support DOS. Client-server turned out to be the right decision, because Quake’s ability to support persistent, come-and-go-as-you-please Internet servers with up to 16 people has been instrumental in the game’s high visibility in the press, and its lasting popularity.

However, client-server is not without a cost, because, in its pure form, latency for clients consists of the round trip from the client to the server and back. (In Quake, orientation changes instantly on the client, short-circuiting the trip to the server, but all other events, such as motion and firing, must make the round trip before they happen on the client.) In peer-to-peer games, maximum latency can be just the cost of the one-way trip, because each client is running a simulation of the game, and each peer sees its own actions instantly. What all this means is that latency is the downside of client-server, but in many other respects client-server is very attractive. So the big task with client-server is to reduce latency.

@@ -52,20 +55,24 @@

The second way in which QuakeWorld attacks latency is by not interpolating. The player is actually predicted well ahead of the latest server packet (after all, the client has all the information needed to move the player, unless an outside force intervenes), giving very responsive control. The rest of the world is drawn as of the latest server packet; this is jerkier than Quake, again showing that smoothness is often a tradeoff for latency. The player’s prediction may, of course, result in a minor paradox; for example, if an explosion turns out to have knocked the player sideways, the player’s location may suddenly jump without warning as the server packet arrives with the correct location. In the latest version of QuakeWorld, the other players are predicted as well, with consequently more frequent paradoxes, but smoother, more convincing motion. Platforms and doors are still not predicted, and consequently are still pretty jerky. It is, of course, possible to predict more and more objects into the future; it’s a tradeoff of smoothness and perceived low latency for the frustration of paradoxes—and that’s the way it’s going to stay until most people are connected to the Internet by something better than modems.

-

Quake 2

+

Quake 2

I can’t talk in detail about Quake 2 as a game, but I can describe some interesting technology features. The Quake 2 rendering engine isn’t going to change that much from Quake; the improvements are largely in areas such as physics, gameplay, artwork, and overall design. The most interesting graphics change is in the preprocessing, where John has added support for radiosity lighting; that is, the ability to put a light source into the world and have the light bounced around the world realistically. This is sometimes terrific—it makes for great glowing light around lava and hanging light panels—but in other cases it’s less spectacular than the effects that designers can get by placing lots of direct-illumination light sources in a room, so the two methods can be used as needed. Also, radiosity is very computationally expensive, approximately as expensive as BSPing. Most of the radiosity demos I’ve seen have been in one or two rooms, and the order of the problem goes up tremendously on whole Quake levels. Here’s another case where the PVS is essential; without it, radiosity processing time would be O(polygons2), but with the PVS it’s O(polygons*average_potentially_visible_polygons), which is over an order of magnitude less (and increases approximately linearly, rather than as a squared function, with greater-level complexity).

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/70-09.html b/70-09.html index cce0fca..ca284ec 100644 --- a/70-09.html +++ b/70-09.html @@ -1,5 +1,4 @@ - + @@ -19,17 +18,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Also, the moving sky texture will probably be gone or will change. One likely replacement is an enclosing texture-mapped box around the world, at a virtually infinite distance; this will allow open vistas, much like Doom, a welcome change from the claustrophobic feel of Quake.

Another likely change in Quake 2 is a shift from interpreted Quake-C code for game logic to compiled DLLs. Part of the incentive here is performance—interpretation isn’t cheap—and part is debugging, because the standard debugger can be used with DLLs. The drawback, of course, is portability; Quake-C program files are completely portable to any platform Quake runs on, with no modification or recompilation, but DLLs compiled for Win32 require a real porting effort to run anywhere else. Our thinking here is that there are almost no non-console platforms other than the PC that matter that much anymore, and for those few that do (notably the Mac and Linux), the DLLs can be ported along with the core engine code. It just doesn’t make sense for easy portability to tiny markets to impose a significant development and performance cost on the one huge market. Consoles will always require serious porting effort anyway, so going to Win32-specific DLLs for the PC version won’t make much difference in the ease of doing console ports.

@@ -54,16 +57,20 @@

Some people worry that the widespread use of hardware acceleration will mean that 3-D programs will all look the same, and that there will no longer be much challenge in 3-D programming. I hope that this brief discussion of the tightly interconnected, highly detailed worlds toward which we’re rapidly heading will help you realize that both the challenge and the potential of 3-D programming are in fact greater than they’ve ever been. The trick is that rather than getting stuck in the rut of established techniques, you must constantly strive to “do better with less, in a different way”; keep learning and changing and trying new approaches—and working your rear end off—and odds are you’ll be part of the wave of the future.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/about.html b/about.html index ff97a02..0451d6b 100644 --- a/about.html +++ b/about.html @@ -1,5 +1,4 @@ - + @@ -17,17 +16,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Foreword

I got my start programming on Apple II computers at school, and almost all of my early work was on the Apple platform. After graduating, it quickly became obvious that I was going to have trouble paying my rent working in the Apple II market in the late eighties, so I was forced to make a very rapid move into the Intel PC environment.

@@ -67,16 +70,20 @@

—John Carmack
id Software

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/about_author.html b/about_author.html index 80d7cb3..55ab7fc 100644 --- a/about_author.html +++ b/about_author.html @@ -1,5 +1,4 @@ - + @@ -18,31 +17,39 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Acknowledgments

There are many people to thank—because this book was written over many years, in many different settings, an unusually large number of people have played a part in making this book possible. Thanks to Dan Illowsky for not only contributing ideas and encouragement, but also getting me started writing articles long ago, when I lacked the confidence to do it on my own—and for teaching me how to handle the business end of things. Thanks to Will Fastie for giving me my first crack at writing for a large audience in the long-gone but still-missed PC Tech Journal, and for showing me how much fun it could be in his even longer-vanished but genuinely terrific column in Creative Computing (the most enjoyable single column I have ever read in a computer magazine; I used to haunt the mailbox around the beginning of the month just to see what Will had to say). Thanks to Robert Keller, Erin O’Connor, Liz Oakley, Steve Baker, and the rest of the cast of thousands that made Programmer’s Journal a uniquely fun magazine—especially Erin, who did more than anyone to teach me the proper use of the English language. (To this day, Erin will still patiently explain to me when one should use “that” and when one should use “which,” even though eight years of instruction on this and related topics have left no discernible imprint on my brain.) Thanks to Tami Zemel, Monica Berg, and the rest of the Dr. Dobb’s Journal crew for excellent, professional editing, and for just being great people. Thanks to the Coriolis gang for their tireless hard work: Jeff Duntemann, Kim Eoff, Jody Kent, Robert Clarfield, and Anthony Stock. Thanks to Jack Tseng for teaching me a lot about graphics hardware, and even more about how much difference hard work can make. Thanks to John Cockerham, David Stafford, Terje Mathisen, the BitMan, Chris Hecker, Jim Mackraz, Melvin Lafitte, John Navas, Phil Coleman, Anton Truenfels, John Carmack, John Miles, John Bridges, Jim Kent, Hal Hardenbergh, Dave Miller, Steve Levy, Jack Davis, Duane Strong, Daev Rohr, Bill Weber, Dan Gochnauer, Patrick Milligan, Tom Wilson, Peter Klerings, Dave Methvin, Mick Brown, the people in the ibm.pc/fast.code topic on Bix, and all the rest of you who have been so generous with your ideas and suggestions. I’ve done my best to acknowledge contributors by name in this book, but if your name is omitted, my apologies, and consider yourself thanked; this book could not have happened without you. And, of course, thanks to Shay and Emily for their generous patience with my passion for writing and computers.

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/appendix-a.html b/appendix-a.html index 4eea79b..3bebf99 100644 --- a/appendix-a.html +++ b/appendix-a.html @@ -1,5 +1,4 @@ - + @@ -18,17 +17,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Afterword

If you’ve followed me this far, you might agree that we’ve come through some rough country. Still, I’m of the opinion that hard-won knowledge is the best knowledge, not only because it sticks to you better, but also because winning a hard race makes it easier to win the next one.

@@ -55,16 +58,20 @@

—Michael Abrash

-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/book-index.html b/book-index.html index 634b75a..b202059 100644 --- a/book-index.html +++ b/book-index.html @@ -1,5 +1,4 @@ - + @@ -18,17 +17,21 @@
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
-


-

Index

@@ -9097,16 +9100,20 @@
-


-
- + - + - +
Previous + Previous + Table of Contents + Table of Contents + Next + Next +
diff --git a/index.html b/index.html index 65e5c69..640440c 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,4 @@ - + @@ -28,11 +27,17 @@
    -
  1. Introduction
  2. +
  3. + Introduction +
  4. -
  5. Foreword
  6. +
  7. + Foreword +
  8. -
  9. About the Author
  10. +
  11. + About the Author +
  12. Part I @@ -42,13 +47,17 @@ Chapter 1—The Best Optimizer Is between Your Ears @@ -86,19 +111,29 @@ Chapter 2—A World Apart @@ -116,49 +153,77 @@ Chapter 3—Assume Nothing @@ -168,15 +233,21 @@ Chapter 4—In the Lair of the Cycle-Eaters @@ -244,19 +345,27 @@ Searching Files with Restartable Blocks
  13. -
  14. Avoiding the String Trap
  15. +
  16. + Avoiding the String Trap +
  17. -
  18. Brute-Force Techniques
  19. +
  20. + Brute-Force Techniques +
  21. Using memchr()
  22. @@ -264,11 +373,15 @@ Interpreting Where the Cycles Go -
  23. Always Look Where Execution Is Going
  24. +
  25. + Always Look Where Execution Is Going +
  26. @@ -280,7 +393,9 @@ How Machine Instructions May Do More Than You Think @@ -288,11 +403,15 @@ Math via Memory Addressing -
  27. Multiplication with LEA Using Non-Powers of Two
  28. +
  29. + Multiplication with LEA Using Non-Powers of Two +
  30. @@ -304,7 +423,9 @@ Optimizing Halfway between Algorithms and Cycle Counting @@ -312,21 +433,31 @@ The Lessons of LOOP and JCXZ -
  31. Local Optimization
  32. +
  33. + Local Optimization +
  34. Unrolling Loops
  35. @@ -340,19 +471,27 @@ Jumping Languages When You Know It’ll Help -
  36. Don’t Call Your Functions on Me, Baby
  37. +
  38. + Don’t Call Your Functions on Me, Baby +
  39. -
  40. Stack Frames Slow So Much
  41. +
  42. + Stack Frames Slow So Much +
  43. Torn Between Two Segments
  44. @@ -360,7 +499,9 @@ Taking It to the Limit @@ -374,25 +515,45 @@ Optimization Odds and Ends from the Field @@ -406,7 +567,9 @@ How Working Quickly Can Bring Execution to a Crawl @@ -414,7 +577,9 @@ The Brute-Force Syndrome @@ -422,7 +587,9 @@ Recursion @@ -436,17 +603,25 @@ New Registers, New Instructions, New Timings, New Complications @@ -490,7 +681,9 @@ It’s Not Just a Bigger 386 @@ -498,9 +691,13 @@ Rules to Optimize By @@ -508,17 +705,27 @@ Caveat Programmor -
  45. The Story Continues
  46. +
  47. + The Story Continues +
  48. @@ -530,17 +737,27 @@ Pipelines and Other Hazards of the High End -
  49. BSWAP: More Useful Than You Might Think
  50. +
  51. + BSWAP: More Useful Than You Might Think +
  52. -
  53. Pushing and Popping Memory
  54. +
  55. + Pushing and Popping Memory +
  56. -
  57. Optimal 1-Bit Shifts and Rotates
  58. +
  59. + Optimal 1-Bit Shifts and Rotates +
  60. -
  61. 32-Bit Addressing Modes
  62. +
  63. + 32-Bit Addressing Modes +
  64. @@ -548,17 +765,29 @@ Chapter 14—Boyer-Moore String Searching @@ -566,15 +795,25 @@ Chapter 15—Linked Lists and plain Unintended Challenges @@ -582,13 +821,17 @@ Chapter 16—There Ain’t No Such Thing as the Fastest Code @@ -628,31 +883,47 @@ Chapter 17—The Game of Life @@ -662,17 +933,25 @@ Chapter 18—It’s a plain Wonderful Life @@ -682,31 +961,47 @@ Chapter 19—Pentium: Not the Same Old Song @@ -716,19 +1011,29 @@ Chapter 20—Pentium Rules @@ -738,25 +1043,35 @@ Chapter 21—Unleashing the Pentium’s V-Pipe @@ -766,9 +1081,13 @@ Chapter 22—Zenning and the Flexible Mind
@@ -782,31 +1101,51 @@ Chapter 23—Bones and Sinew @@ -814,11 +1153,17 @@ Chapter 24—Parallel Processing with the VGA @@ -826,25 +1171,39 @@ Chapter 25—VGA Data Machinery @@ -852,11 +1211,17 @@ Chapter 26—VGA Write Mode 3 @@ -864,25 +1229,39 @@ Chapter 27—Yet Another VGA Write Mode @@ -890,13 +1269,21 @@ Chapter 28—Reading VGA Memory @@ -904,17 +1291,29 @@ Chapter 29—Saving Screens and Other VGA Mysteries @@ -922,35 +1321,53 @@ Chapter 30—Video Est Omnis Divisa @@ -958,23 +1375,35 @@ Chapter 31—Higher 256-Color Resolution on the VGA @@ -982,21 +1411,33 @@ Chapter 32—Be It Resolved: 360x480 @@ -1006,29 +1447,47 @@ Chapter 33—Yogi Bear and Eurythmics Confront VGA Colors @@ -1036,33 +1495,51 @@ Chapter 34—Changing Colors without Writing Pixels @@ -1072,15 +1549,21 @@ Chapter 35—Bresenham Is Fast, and Fast Is Good @@ -1106,13 +1599,21 @@ Chapter 36—The Good, the Bad, and the Run-Sliced @@ -1120,15 +1621,21 @@ Chapter 37—Dead Cats and Lightning Lines @@ -1138,21 +1645,31 @@ Chapter 38—The Polygon Primeval @@ -1160,15 +1677,21 @@ Chapter 39—Fast Convex Polygons @@ -1188,13 +1715,17 @@ Chapter 40—Of Songs, Taxes, and the Simplicity of Complex Polygons @@ -1222,9 +1759,13 @@ Chapter 41—Those Way-Down Polygon Nomenclature Blues @@ -1232,17 +1773,25 @@ Chapter 42—Wu’ed in Haste; Fried, Stewed at Leisure @@ -1252,23 +1801,35 @@ Chapter 43—Bit-Plane Animation @@ -1280,23 +1841,35 @@ 640x480 Page Flipped Animation in 64K...Almost @@ -1306,25 +1879,39 @@ Chapter 45—Dog Hair and Dirty Rectangles @@ -1336,19 +1923,27 @@ Optimizing Dirty-Rectangle Animation @@ -1358,15 +1953,25 @@ Chapter 47—Mode X: 256-Color VGA Magic @@ -1374,19 +1979,27 @@ Chapter 48—Mode X Marks the Latch @@ -1394,23 +2007,35 @@ Chapter 49—Mode X 256-Color Animation @@ -1418,19 +2043,29 @@ Chapter 50—Adding a Dimension @@ -1450,21 +2089,31 @@ Chapter 51—Sneakers in Space @@ -1472,19 +2121,29 @@ Chapter 52—Fast 3-D Animation: Meet X-Sharp @@ -1494,17 +2153,25 @@ Chapter 53—Raw Speed and More @@ -1514,7 +2181,9 @@ Chapter 54—3-D Shading @@ -1544,9 +2219,13 @@ Pondering X-Sharp’s Color Model in an RGB State of Mind @@ -1556,17 +2235,23 @@ Chapter 56—Pooh and the Space Station @@ -1578,17 +2263,25 @@ The Critical Role of Experience in Implementing Fast, Smooth Texture Mapping @@ -1600,15 +2293,21 @@ Chapter 58—Heinlein’s Crystal Ball, Spock’s Brain, and the 9-Cycle Dare @@ -1628,15 +2331,21 @@ Chapter 59—The Idea of BSP Trees @@ -1672,23 +2389,35 @@ Chapter 60—Compiling BSP Trees @@ -1700,9 +2429,13 @@ The Fundamentals of the Math behind 3-D Graphics @@ -1710,17 +2443,27 @@ The Dot Product -
  • Cross Products and the Generation of Polygon Normals
  • +
  • + Cross Products and the Generation of Polygon Normals +
  • -
  • Using the Sign of the Dot Product
  • +
  • + Using the Sign of the Dot Product +
  • -
  • Using the Dot Product for Projection
  • +
  • + Using the Dot Product for Projection +
  • -
  • Rotation by Projection
  • +
  • + Rotation by Projection +
  • @@ -1732,25 +2475,39 @@ Taking a Compiled BSP Tree from Logical to Visual Reality -
  • Moving the Viewer
  • +
  • + Moving the Viewer +
  • -
  • Transformation into Viewspace
  • +
  • + Transformation into Viewspace +
  • -
  • Clipping
  • +
  • + Clipping +
  • -
  • Projection to Screenspace
  • +
  • + Projection to Screenspace +
  • Walking the Tree, Backface Culling and Drawing
  • @@ -1760,31 +2517,51 @@ Chapter 63—Floating-Point for Real-Time 3-D @@ -1792,47 +2569,77 @@ Chapter 64—Quake’s Visible-Surface Determination @@ -1840,13 +2647,17 @@ Chapter 65—3-D Clipping and Other Thoughts @@ -1870,35 +2689,53 @@ Chapter 66—Quake’s Hidden-Surface Removal @@ -1906,19 +2743,29 @@ Chapter 67—Sorted Spans in Action @@ -1936,17 +2785,25 @@ Chapter 68—Quake’s Lighting Model @@ -1976,15 +2841,21 @@ Chapter 69—Surface Caching and Quake’s Triangle Models @@ -2010,21 +2891,33 @@ Chapter 70—Quake: A Post-Mortem and a Glimpse into the Future -
  • Appendix A
  • +
  • + Appendix A +
  • -
  • Index
  • +
  • + Index +

  • diff --git a/intro.html b/intro.html index 4e3b4f6..2e6d2f8 100644 --- a/intro.html +++ b/intro.html @@ -1,5 +1,4 @@ - + @@ -17,17 +16,21 @@
    - + - + - +
    Previous + Previous + Table of Contents + Table of Contents + Next + Next +
    -


    -

    Introduction

    What was it like working with John Carmack on Quake? Like being strapped onto a rocket during takeoff—in the middle of a hurricane. It seemed like the whole world was watching, waiting to see if id Software could top Doom; every casual e-mail tidbit or conversation with a visitor ended up posted on the Internet within hours. And meanwhile, we were pouring everything we had into Quake’s technology; I’d often come in in the morning to find John still there, working on a new idea so intriguing that he couldn’t bear to sleep until he had tried it out. Toward the end, when I spent most of my time speeding things up, I would spend the day in a trance writing optimized assembly code, stagger out of the Town East Tower into the blazing Texas heat, and somehow drive home on LBJ Freeway without smacking into any of the speeding pickups whizzing past me on both sides. At home, I’d fall into a fitful sleep, then come back the next day in a daze and do it again. Everything happened so fast, and under so much pressure, that sometimes I wonder how any of us made it through that without completely burning out.

    @@ -50,16 +53,20 @@ Bellevue, Washington
    May 1997

    -


    -
    - + - + - +
    Previous + Previous + Table of Contents + Table of Contents + Next + Next +