abrash-black-book/52-08.md
2013-12-30 20:26:41 +11:00

87 lines
4.9 KiB
Markdown

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.
The third performance key is maintaining and operating on only the
relevant portions of transformation matrices and coordinates. The bottom
row of every transformation matrix we'll use (in this book) is [0 0 0
1], so why bother using or recalculating it when concatenating
transforms and transforming points? Likewise for the fourth element of a
3-D vector in homogeneous coordinates, which is always 1. Basically,
transformation matrices are treated as consisting of a 3x3 rotation
matrix and a 3x1 translation vector, and coordinates are treated as 3x1
vectors. This saves a great many multiplications in the course of
transforming each point.
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 {#Heading6}
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 {#Heading7}
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.