Move source markdown files into src/
This commit is contained in:
parent
9deba4ffa6
commit
3736ff43be
76 changed files with 1 additions and 1 deletions
753
src/chapter-39.md
Normal file
753
src/chapter-39.md
Normal file
|
|
@ -0,0 +1,753 @@
|
|||
---
|
||||
title: Michael Abrash's Graphics Programming Black Book, Special Edition
|
||||
author: Michael Abrash
|
||||
date: '1997-07-01'
|
||||
identifier:
|
||||
- scheme: ISBN
|
||||
text: 1576101746
|
||||
publisher: The Coriolis Group
|
||||
category: 'Web and Software Development: Game Development,Web and Software Development:
|
||||
Graphics and Multimedia Development'
|
||||
chapter: '39'
|
||||
pages: 723-738
|
||||
---
|
||||
|
||||
## Chapter 39 -- Fast Convex Polygons
|
||||
|
||||
### Filling Polygons in a Hurry
|
||||
|
||||
In the previous chapter, we explored the surprisingly intricate process
|
||||
of filling convex polygons. Now we're going to fill them an order of
|
||||
magnitude or so faster.
|
||||
|
||||
Two thoughts may occur to some of you at this point: "Oh, no, he's not
|
||||
going to get into assembly language and device-dependent code, is he?"
|
||||
and, "Why bother with polygon filling—or, indeed, any drawing
|
||||
primitives—anyway? Isn't that what GUIs and third-party libraries are
|
||||
for?"
|
||||
|
||||
To which I answer, "Well, yes, I am," and, "If you have to ask, you've
|
||||
missed the magic of microcomputer programming." Actually, both questions
|
||||
ask the same thing, and that is: "Why should I, as a programmer, have
|
||||
any idea how my program actually works?"
|
||||
|
||||
Put that way, it sounds a little different, doesn't it?
|
||||
|
||||
GUIs, reusable code, portable code written entirely in high-level
|
||||
languages, and object-oriented programming are all the rage now, and
|
||||
promise to remain so for the foreseeable future. The thrust of this
|
||||
technology is to enhance the software development process by offloading
|
||||
as much responsibility as possible to other programmers, and by writing
|
||||
all remaining code in modular, generic form. This modular code then
|
||||
becomes a black box to be reused endlessly without another thought about
|
||||
what actually lies inside. GUIs also reduce development time by making
|
||||
many interface choices for you. That, in turn, makes it possible to
|
||||
create quickly and reliably programs that will be easy for new users to
|
||||
pick up, so software becomes easier to both produce and learn. This is,
|
||||
without question, a Good Thing.
|
||||
|
||||
The "black box" approach does not, however, necessarily cause the
|
||||
software itself to become faster, smaller, or more innovative; quite the
|
||||
opposite, I suspect. I'll reserve judgement on whether that is a good
|
||||
thing or not, but I'll make a prediction: In the short run, the
|
||||
aforementioned techniques will lead to noticeably larger, slower
|
||||
programs, as programmers understand less and less of what the key parts
|
||||
of their programs do and rely increasingly on general-purpose code
|
||||
written by other people. (In the long run, programs will be bigger and
|
||||
slower yet, but computers will be so fast and will have so much memory
|
||||
that no one will care.) Over time, PC programs will also come to be more
|
||||
similar to one another—and to programs running on other platforms, such
|
||||
as the Mac—as regards both user interface and performance.
|
||||
|
||||
Again, I am not saying that this is bad. It does, however, have major
|
||||
implications for the future nature of PC graphics programming, in ways
|
||||
that will directly affect the means by which many of you earn your
|
||||
livings. Not so very long from now, graphics programming—all
|
||||
programming, for that matter—will become mostly a matter of assembling
|
||||
in various ways components written by other people, and will cease to be
|
||||
the all-inclusively creative, mindbendingly complex pursuit it is today.
|
||||
(Using legally certified black boxes is, by the way, one direction in
|
||||
which the patent lawyers are leading us; legal considerations may be the
|
||||
final nail in the coffin of homegrown code.) For now, though, it's still
|
||||
within your power, as a PC programmer, to understand and even control
|
||||
every single thing that happens on a computer if you so desire, to
|
||||
realize any vision you may have. Take advantage of this unique window of
|
||||
opportunity to create some magic!
|
||||
|
||||
Neither does it hurt to understand what's involved in drawing, say, a
|
||||
filled polygon, even if you are using a GUI. You will better understand
|
||||
the performance implications of the available GUI functions, and you
|
||||
will be able to fill in any gaps in the functions provided. You may even
|
||||
find that you can outperform the GUI on occasion by doing your own
|
||||
drawing into a system memory bitmap, then copying the result to the
|
||||
screen; for instance, you can do this under Windows by using the WinG
|
||||
library available from Microsoft. You will also be able to understand
|
||||
why various quirks exist, and will be able to put them to good use. For
|
||||
example, the X Window System follows the polygon drawing rules described
|
||||
in the previous chapter (although it's not obvious from the X Window
|
||||
System documentation); if you understood the previous chapter's
|
||||
discussion, you're in good shape to use polygons under X.
|
||||
|
||||
In short, even though doing so runs counter to current trends, it helps
|
||||
to understand how things work, especially when they're very visible
|
||||
parts of the software you develop. That said, let's learn more about
|
||||
filling convex polygons.
|
||||
|
||||
### Fast Convex Polygon Filling
|
||||
|
||||
In addressing the topic of filling convex polygons in the previous
|
||||
chapter, the implementation we came up with met all of our functional
|
||||
requirements. In particular, it met stringent rules that guaranteed that
|
||||
polygons would never overlap or have gaps at shared edges, an important
|
||||
consideration when building polygon-based images. Unfortunately, the
|
||||
implementation was also slow as molasses. In this chapter we'll work up
|
||||
polygon-filling code that's fast enough to be truly usable.
|
||||
|
||||
Our original polygon filling code involved three major tasks, each
|
||||
performed by a separate function:
|
||||
|
||||
* Tracing each polygon edge to generate a coordinate list (performed
|
||||
by the function `ScanEdge`);
|
||||
* Drawing the scanned-out horizontal lines that constitute the filled
|
||||
polygon (`DrawHorizontalLineList`); and
|
||||
* Characterizing the polygon and coordinating the tracing and drawing
|
||||
(`FillConvexPolygon`).
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
| Implementation | Total Polygon Filling Time | DrawHorizontal LineList | ScanEdge | FillConvex Polygon |
|
||||
|-----------------------------------------------------------------------------|----------------------------|-----------------------------|------------|--------------------|
|
||||
| Drawing to display memory in mode 13h | | | | |
|
||||
| C floating point scan/DrawPixel drawing code from Chapter 38, (small model) | 11.69 | 5.80 seconds (50% of total) | 5.86 (50%) | 0.03 (<1%) |
|
||||
| C floating point scan/memset drawing (Listing 39.1, compact model) | 6.64 | 0.49 (7%) | 6.11 (92%) | 0.04 (<1%) |
|
||||
| C integer scan/memset drawing (Listing 39.1 & Listing 39.2, compact model) | 0.60 | 0.49 (82%) | 0.07 (12%) | 0.04 (7%) |
|
||||
| C integer scan/ASM drawing (Listing 39.2 & Listing 39.3, small model) | 0.45 | 0.36 (80%) | 0.06 (13%) | 0.03 (7%) |
|
||||
| ASM integer scan/ASM drawing (Listing 40.3 & Listing 40.4, small model) | 0.42 | 0.36 (86%) | 0.03 (7%) | 0.03 (7%) |
|
||||
| Drawing to system memory | | | | |
|
||||
| C integer scan/memset drawing (Listing 39.1 & Listing 39.2, compact model) | 0.31 | 0.20 (65%) | 0.07 (23%) | 0.04 (13%) |
|
||||
| ASM integer scan/ASM drawing (Listing 39.3 & Listing 39.4, small model) | 0.13 | 0.07 (54%) | 0.03 (23%) | 0.03 (23%) |
|
||||
|
||||
Table: Table 39.1 Polygon fill performance.
|
||||
|
||||
All times are in seconds, as measured with Turbo Profiler on a 20-MHz
|
||||
cached 386 with no math coprocessor installed. Note that time spent in
|
||||
`main()` is not included. C code was compiled with Borland C++ with
|
||||
maximum optimization (-G -O -Z -r -a); assembly language code was
|
||||
assembled with TASM. Percentages of combined times are rounded to the
|
||||
nearest percent, so the sum of the three percentages does not always
|
||||
equal 100.
|
||||
|
||||
**LISTING 39.1 L39-1.C**
|
||||
|
||||
```c
|
||||
/* Draws all pixels in the list of horizontal lines passed in, in
|
||||
mode 13h, the VGA's 320x200 256-color mode. Uses memset to fill
|
||||
each line, which is much faster than using DrawPixel but requires
|
||||
that a large data model (compact, large, or huge) be in use when
|
||||
running in real mode or 286 protected mode.
|
||||
All C code tested with Borland C++. */
|
||||
|
||||
#include <string.h>
|
||||
#include <dos.h>
|
||||
#include "polygon.h"
|
||||
|
||||
#define SCREEN_WIDTH 320
|
||||
#define SCREEN_SEGMENT 0xA000
|
||||
|
||||
void DrawHorizontalLineList(struct HLineList * HLineListPtr,
|
||||
int Color)
|
||||
{
|
||||
struct HLine *HLinePtr;
|
||||
int Length, Width;
|
||||
unsigned char far *ScreenPtr;
|
||||
|
||||
/* Point to the start of the first scan line on which to draw */
|
||||
ScreenPtr = MK_FP(SCREEN_SEGMENT,
|
||||
HLineListPtr->YStart * SCREEN_WIDTH);
|
||||
|
||||
/* Point to the XStart/XEnd descriptor for the first (top)
|
||||
horizontal line */
|
||||
HLinePtr = HLineListPtr->HLinePtr;
|
||||
/* Draw each horizontal line in turn, starting with the top one and
|
||||
advancing one line each time */
|
||||
Length = HLineListPtr->Length;
|
||||
while (Length-- > 0) {
|
||||
/* Draw the whole horizontal line if it has a positive width */
|
||||
if ((Width = HLinePtr->XEnd - HLinePtr->XStart + 1) > 0)
|
||||
memset(ScreenPtr + HLinePtr->XStart, Color, Width);
|
||||
HLinePtr++; /* point to next scan line X info */
|
||||
ScreenPtr += SCREEN_WIDTH; /* point to next scan line start */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At this point, I'd like to mention that benchmarks are notoriously
|
||||
unreliable; the results in Table 39.1 are accurate *only* for the test
|
||||
program, and only when running on a particular system. Results could be
|
||||
vastly different if smaller, larger, or more complex polygons were
|
||||
drawn, or if a faster or slower computer/VGA combination were used.
|
||||
These factors notwithstanding, the test program does fill a variety of
|
||||
polygons of varying complexity sized from large to small and in between,
|
||||
and certainly the order of magnitude difference between Listing 39.1 and
|
||||
the old version of `DrawHorizontalLineList` is a clear indication of
|
||||
which code is superior.
|
||||
|
||||
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
|
||||
|
||||
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?
|
||||
|
||||
In fact, when I tested out the integer edge tracing code by comparing an
|
||||
integer-based test image to one produced by floating-point calculations,
|
||||
two pixels out of the whole screen differed, leading me to suspect a bug
|
||||
in the integer code. It turned out, however, that's in those two cases,
|
||||
the floating point results were sufficiently imprecise to creep from
|
||||
just under an integer value to just over it, so that the `ceil`
|
||||
function returned a coordinate that was one too large.
|
||||
|
||||
> 
|
||||
> Floating point is very accurate—but it is not precise. Integer
|
||||
> calculations, properly performed, are.
|
||||
|
||||
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.
|
||||
|
||||
**LISTING 39.2 L39-2.C**
|
||||
|
||||
```c
|
||||
/* Scan converts an edge from (X1,Y1) to (X2,Y2), not including the
|
||||
point at (X2,Y2). If SkipFirst == 1, the point at (X1,Y1) isn't
|
||||
drawn; if SkipFirst == 0, it is. For each scan line, the pixel
|
||||
closest to the scanned edge without being to the left of the
|
||||
scanned edge is chosen. Uses an all-integer approach for speed and
|
||||
precision. */
|
||||
|
||||
#include <math.h>
|
||||
#include "polygon.h"
|
||||
|
||||
void ScanEdge(int X1, int Y1, int X2, int Y2, int SetXStart,
|
||||
int SkipFirst, struct HLine **EdgePointPtr)
|
||||
{
|
||||
int Y, DeltaX, Height, Width, AdvanceAmt, ErrorTerm, i;
|
||||
int ErrorTermAdvance, XMajorAdvanceAmt;
|
||||
struct HLine *WorkingEdgePointPtr;
|
||||
|
||||
WorkingEdgePointPtr = *EdgePointPtr; /* avoid double dereference */
|
||||
AdvanceAmt = ((DeltaX = X2 - X1) > 0) ? 1 : -1;
|
||||
/* direction in which X moves (Y2 is
|
||||
always > Y1, so Y always counts up) */
|
||||
|
||||
if ((Height = Y2 - Y1) <= 0) /* Y length of the edge */
|
||||
return; /* guard against 0-length and horizontal edges */
|
||||
|
||||
/* Figure out whether the edge is vertical, diagonal, X-major
|
||||
(mostly horizontal), or Y-major (mostly vertical) and handle
|
||||
appropriately */
|
||||
if ((Width = abs(DeltaX)) == 0) {
|
||||
/* The edge is vertical; special-case by just storing the same
|
||||
X coordinate for every scan line */
|
||||
/* Scan the edge for each scan line in turn */
|
||||
for (i = Height - SkipFirst; i-- > 0; WorkingEdgePointPtr++) {
|
||||
/* Store the X coordinate in the appropriate edge list */
|
||||
if (SetXStart == 1)
|
||||
WorkingEdgePointPtr->XStart = X1;
|
||||
else
|
||||
WorkingEdgePointPtr->XEnd = X1;
|
||||
}
|
||||
} else if (Width == Height) {
|
||||
/* The edge is diagonal; special-case by advancing the X
|
||||
coordinate 1 pixel for each scan line */
|
||||
if (SkipFirst) /* skip the first point if so indicated */
|
||||
X1 += AdvanceAmt; /* move 1 pixel to the left or right */
|
||||
/* Scan the edge for each scan line in turn */
|
||||
for (i = Height - SkipFirst; i-- > 0; WorkingEdgePointPtr++) {
|
||||
/* Store the X coordinate in the appropriate edge list */
|
||||
if (SetXStart == 1)
|
||||
WorkingEdgePointPtr->XStart = X1;
|
||||
else
|
||||
WorkingEdgePointPtr->XEnd = X1;
|
||||
X1 += AdvanceAmt; /* move 1 pixel to the left or right */
|
||||
}
|
||||
} else if (Height > Width) {
|
||||
/* Edge is closer to vertical than horizontal (Y-major) */
|
||||
if (DeltaX >= 0)
|
||||
ErrorTerm = 0; /* initial error term going left->right */
|
||||
else
|
||||
ErrorTerm = -Height + 1; /* going right->left */
|
||||
if (SkipFirst) { /* skip the first point if so indicated */
|
||||
/* Determine whether it's time for the X coord to advance */
|
||||
if ((ErrorTerm += Width) > 0) {
|
||||
X1 += AdvanceAmt; /* move 1 pixel to the left or right */
|
||||
ErrorTerm -= Height; /* advance ErrorTerm to next point */
|
||||
}
|
||||
}
|
||||
/* Scan the edge for each scan line in turn */
|
||||
for (i = Height - SkipFirst; i-- > 0; WorkingEdgePointPtr++) {
|
||||
/* Store the X coordinate in the appropriate edge list */
|
||||
if (SetXStart == 1)
|
||||
WorkingEdgePointPtr->XStart = X1;
|
||||
else
|
||||
WorkingEdgePointPtr->XEnd = X1;
|
||||
/* Determine whether it's time for the X coord to advance */
|
||||
if ((ErrorTerm += Width) > 0) {
|
||||
X1 += AdvanceAmt; /* move 1 pixel to the left or right */
|
||||
ErrorTerm -= Height; /* advance ErrorTerm to correspond */
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Edge is closer to horizontal than vertical (X-major) */
|
||||
/* Minimum distance to advance X each time */
|
||||
XMajorAdvanceAmt = (Width / Height) * AdvanceAmt;
|
||||
/* Error term advance for deciding when to advance X 1 extra */
|
||||
ErrorTermAdvance = Width % Height;
|
||||
if (DeltaX >= 0)
|
||||
ErrorTerm = 0; /* initial error term going left->right */
|
||||
else
|
||||
ErrorTerm = -Height + 1; /* going right->left */
|
||||
if (SkipFirst) { /* skip the first point if so indicated */
|
||||
X1 += XMajorAdvanceAmt; /* move X minimum distance */
|
||||
/* Determine whether it's time for X to advance one extra */
|
||||
if ((ErrorTerm += ErrorTermAdvance) > 0) {
|
||||
X1 += AdvanceAmt; /* move X one more */
|
||||
ErrorTerm -= Height; /* advance ErrorTerm to correspond */
|
||||
}
|
||||
}
|
||||
/* Scan the edge for each scan line in turn */
|
||||
for (i = Height - SkipFirst; i-- > 0; WorkingEdgePointPtr++) {
|
||||
/* Store the X coordinate in the appropriate edge list */
|
||||
if (SetXStart == 1)
|
||||
WorkingEdgePointPtr->XStart = X1;
|
||||
else
|
||||
WorkingEdgePointPtr->XEnd = X1;
|
||||
X1 += XMajorAdvanceAmt; /* move X minimum distance */
|
||||
/* Determine whether it's time for X to advance one extra */
|
||||
if ((ErrorTerm += ErrorTermAdvance) > 0) {
|
||||
X1 += AdvanceAmt; /* move X one more */
|
||||
ErrorTerm -= Height; /* advance ErrorTerm to correspond */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*EdgePointPtr = WorkingEdgePointPtr; /* advance caller's ptr */
|
||||
}
|
||||
```
|
||||
|
||||
### The Finishing Touch: Assembly Language
|
||||
|
||||
The C implementation in Listing 39.2 is now nearly 20 times as fast as
|
||||
the original, which is good enough for most purposes. Still, it requires
|
||||
that one of the large data models be used (for `memset` ), and it's
|
||||
certainly not the fastest possible code. The obvious next step is
|
||||
assembly language.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
> 
|
||||
> There is a lesson here. An optimization has no fixed payoff; its value
|
||||
> fluctuates according to the context in which it is used. There's
|
||||
> relatively little benefit to further optimizing code that already spends
|
||||
> half its time waiting for display memory; no matter how good your
|
||||
> optimizations, you'll get only a two-times speedup at best, and
|
||||
> generally much less than that. There is, on the other hand, potential
|
||||
> for tremendous improvement when drawing to system memory, so if that's
|
||||
> where most of your drawing will occur, optimizations such as Listing
|
||||
> 39.3 are well worth the effort.
|
||||
>
|
||||
> Know the environments in which your code will run, and know where the
|
||||
> cycles go in those environments.
|
||||
|
||||
**LISTING 39.3 L39-3.ASM**
|
||||
|
||||
```nasm
|
||||
; Draws all pixels in the list of horizontal lines passed in, in
|
||||
; mode 13h, the VGA's 320x200 256-color mode. Uses REP STOS to fill
|
||||
; each line.
|
||||
; C near-callable as:
|
||||
; void DrawHorizontalLineList(struct HLineList * HLineListPtr,
|
||||
; int Color);
|
||||
; All assembly code tested with TASM and MASM
|
||||
|
||||
SCREEN_WIDTH equ 320
|
||||
SCREEN_SEGMENT equ 0a000h
|
||||
|
||||
HLinestruc
|
||||
XStart dw ? ;X coordinate of leftmost pixel in line
|
||||
XEnd dw ? ;X coordinate of rightmost pixel in line
|
||||
HLine ends
|
||||
|
||||
HLineList struc
|
||||
Lngth dw ? ;# of horizontal lines
|
||||
YStart dw ? ;Y coordinate of topmost line
|
||||
HLinePtr dw ? ;pointer to list of horz lines
|
||||
HLineList ends
|
||||
|
||||
Parms struc
|
||||
dw 2 dup(?) ;return address & pushed BP
|
||||
HLineListPtr dw ? ;pointer to HLineList structure
|
||||
Color dw ? ;color with which to fill
|
||||
Parms ends
|
||||
|
||||
.model small
|
||||
.code
|
||||
public _DrawHorizontalLineList
|
||||
align 2
|
||||
_DrawHorizontalLineList proc
|
||||
push bp ;preserve caller's stack frame
|
||||
mov bp,sp ;point to our stack frame
|
||||
push si ;preserve caller's register variables
|
||||
push di
|
||||
cld ;make string instructions inc pointers
|
||||
|
||||
mov ax,SCREEN_SEGMENT
|
||||
mov es,ax ;point ES to display memory for REP STOS
|
||||
|
||||
mov si,[bp+HLineListPtr] ;point to the line list
|
||||
mov ax,SCREEN_WIDTH ;point to the start of the first scan
|
||||
mul [si+YStart] ; line in which to draw
|
||||
mov dx,ax ;ES:DX points to first scan line to
|
||||
; draw
|
||||
mov bx,[si+HLinePtr] ;point to the XStart/XEnd descriptor
|
||||
; for the first (top) horizontal line
|
||||
mov si,[si+Lngth] ;# of scan lines to draw
|
||||
and si,si ;are there any lines to draw?
|
||||
jz FillDone ;no, so we're done
|
||||
mov al,byte ptr [bp+Color];color with which to fill
|
||||
mov ah,al ;duplicate color for STOSW
|
||||
FillLoop:
|
||||
mov di,[bx+XStart] ;left edge of fill on this line
|
||||
mov cx,[bx+XEnd] ;right edge of fill
|
||||
sub cx,di
|
||||
js LineFillDone ;skip if negative width
|
||||
inc cx ;width of fill on this line
|
||||
add di,dx ;offset of left edge of fill
|
||||
test di,1 ;does fill start at an odd address?
|
||||
jz MainFill ;no
|
||||
stosb ;yes, draw the odd leading byte to
|
||||
; word-align the rest of the fill
|
||||
dec cx ;count off the odd leading byte
|
||||
jz LineFillDone ;done if that was the only byte
|
||||
MainFill:
|
||||
shr cx,1 ;# of words in fill
|
||||
rep stosw ;fill as many words as possible
|
||||
adc cx,cx ;1 if there's an odd trailing byte to
|
||||
; do, 0 otherwise
|
||||
rep stosb ;fill any odd trailing byte
|
||||
LineFillDone:
|
||||
add bx,size HLine ;point to the next line descriptor
|
||||
add dx,SCREEN_WIDTH ;point to the next scan line
|
||||
dec si ;count off lines to fill
|
||||
jnz FillLoop
|
||||
FillDone:
|
||||
pop di ;restore caller's register variables
|
||||
pop si
|
||||
pop bp ;restore caller's stack frame
|
||||
ret
|
||||
_DrawHorizontalLineList endp
|
||||
end
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
Listing 39.3 is another case in which it's worth knowing the environment
|
||||
in which your code will run. Extra code is required to perform aligned
|
||||
word-at-a-time filling, resulting in extra overhead. For very small or
|
||||
narrow polygons, that overhead might overwhelm the advantage of drawing
|
||||
a word at a time, making plain old `REP STOSB` faster.
|
||||
|
||||
### Faster Edge Tracing
|
||||
|
||||
Finally, Listing 39.4 is an assembly language version of `ScanEdge`.
|
||||
Listing 39.4 is a relatively straightforward translation from C to
|
||||
assembly, but is nonetheless about twice as fast as Listing 39.2.
|
||||
|
||||
The version of `ScanEdge` in Listing 39.4 could certainly be sped up
|
||||
still further by unrolling the loops. `FillConvexPolygon`, the overall
|
||||
coordination routine, hasn't even been converted to assembly language,
|
||||
so that could be sped up as well. I haven't bothered with these
|
||||
optimizations because all code other than `DrawHorizontalLineList`
|
||||
takes only 14 percent of the overall polygon filling time when drawing
|
||||
to display memory; the potential return on optimizing nondrawing code
|
||||
simply isn't great enough to justify the effort. Part of the value of a
|
||||
profiler is being able to tell when to stop optimizing; with Listings
|
||||
39.3 and 39.4 in use, more than two-thirds of the time taken to draw
|
||||
polygons is spent waiting for display memory, so optimization is pretty
|
||||
much maxed out. However, further optimization might be worthwhile when
|
||||
drawing to system memory, where wait states are out of the picture and
|
||||
the nondrawing code takes a significant portion (46 percent) of the
|
||||
overall time.
|
||||
|
||||
Again, *know where the cycles go* .
|
||||
|
||||
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.
|
||||
|
||||
**LISTING 39.4 L39-4.ASM**
|
||||
|
||||
```nasm
|
||||
; Scan converts an edge from (X1,Y1) to (X2,Y2), not including the
|
||||
; point at (X2,Y2). If SkipFirst == 1, the point at (X1,Y1) isn't
|
||||
; drawn; if SkipFirst == 0, it is. For each scan line, the pixel
|
||||
; closest to the scanned edge without being to the left of the scanned
|
||||
; edge is chosen. Uses an all-integer approach for speed & precision.
|
||||
; C near-callable as:
|
||||
; void ScanEdge(int X1, int Y1, int X2, int Y2, int SetXStart,
|
||||
; int SkipFirst, struct HLine **EdgePointPtr);
|
||||
; Edges must not go bottom to top; that is, Y1 must be <= Y2.
|
||||
; Updates the pointer pointed to by EdgePointPtr to point to the next
|
||||
; free entry in the array of HLine structures.
|
||||
|
||||
HLine struc
|
||||
XStart dw ? ;X coordinate of leftmost pixel in scan line
|
||||
XEnd dw ? ;X coordinate of rightmost pixel in scan line
|
||||
HLine ends
|
||||
|
||||
Parms struc
|
||||
dw 2 dup(?) ;return address & pushed BP
|
||||
X1 dw ? ;X start coord of edge
|
||||
Y1 dw ? ;Y start coord of edge
|
||||
X2 dw ? ;X end coord of edge
|
||||
Y2 dw ? ;Y end coord of edge
|
||||
SetXStart dw ? ;1 to set the XStart field of each
|
||||
; HLine struc, 0 to set XEnd
|
||||
SkipFirst dw ? ;1 to skip scanning the first point
|
||||
; of the edge, 0 to scan first point
|
||||
EdgePointPtr dw ? ;pointer to a pointer to the array of
|
||||
; HLine structures in which to store
|
||||
; the scanned X coordinates
|
||||
Parms ends
|
||||
|
||||
;Offsets from BP in stack frame of local variables.
|
||||
AdvanceAmt equ -2
|
||||
Height equ -4
|
||||
LOCAL_SIZE equ 4 ;total size of local variables
|
||||
|
||||
.model small
|
||||
.code
|
||||
public _ScanEdge
|
||||
align 2
|
||||
_ScanEdge proc
|
||||
push bp ;preserve caller's stack frame
|
||||
mov bp,sp ;point to our stack frame
|
||||
sub sp,LOCAL_SIZE ;allocate space for local variables
|
||||
push si ;preserve caller's register variables
|
||||
push di
|
||||
mov di,[bp+EdgePointPtr]
|
||||
mov di,[di] ;point to the HLine array
|
||||
cmp [bp+SetXStart],1 ;set the XStart field of each HLine
|
||||
; struc?
|
||||
jz HLinePtrSet ;yes, DI points to the first XStart
|
||||
add di,XEnd ;no, point to the XEnd field of the
|
||||
; first HLine struc
|
||||
HLinePtrSet:
|
||||
mov bx,[bp+Y2]
|
||||
sub bx,[bp+Y1] ;edge height
|
||||
jle ToScanEdgeExit ;guard against 0-length & horz edges
|
||||
mov [bp+Height],bx ;Height = Y2 - Y1
|
||||
sub cx,cx ;assume ErrorTerm starts at 0 (true if
|
||||
; we're moving right as we draw)
|
||||
mov dx,1 ;assume AdvanceAmt = 1 (move right)
|
||||
mov ax,[bp+X2]
|
||||
sub ax,[bp+X1] ;DeltaX = X2 - X1
|
||||
jz IsVertical ;it's a vertical edge--special case it
|
||||
jns SetAdvanceAmt ;DeltaX >= 0
|
||||
mov cx,1 ;DeltaX < 0 (move left as we draw)
|
||||
sub cx,bx ;ErrorTerm = -Height + 1
|
||||
neg dx ;AdvanceAmt = -1 (move left)
|
||||
neg ax ;Width = abs(DeltaX)
|
||||
SetAdvanceAmt:
|
||||
mov [bp+AdvanceAmt],dx
|
||||
; Figure out whether the edge is diagonal, X-major (more horizontal),
|
||||
; or Y-major (more vertical) and handle appropriately.
|
||||
cmp ax,bx ;if Width==Height, it's a diagonal edge
|
||||
jz IsDiagonal ;it's a diagonal edge--special case
|
||||
jb YMajor ;it's a Y-major (more vertical) edge
|
||||
;it's an X-major (more horz) edge
|
||||
sub dx,dx ;prepare DX:AX (Width) for division
|
||||
div bx ;Width/Height
|
||||
;DX = error term advance per scan line
|
||||
mov si,ax ;SI = minimum # of pixels to advance X
|
||||
; on each scan line
|
||||
test [bp+AdvanceAmt],8000h ;move left or right?
|
||||
jz XMajorAdvanceAmtSet ;right, already set
|
||||
neg si ;left, negate the distance to advance
|
||||
; on each scan line
|
||||
XMajorAdvanceAmtSet: ;
|
||||
mov ax,[bp+X1] ;starting X coordinate
|
||||
cmp [bp+SkipFirst],1 ;skip the first point?
|
||||
jz XMajorSkipEntry ;yes
|
||||
XMajorLoop:
|
||||
mov [di],ax ;store the current X value
|
||||
add di,size HLine ;point to the next HLine struc
|
||||
XMajorSkipEntry:
|
||||
add ax,si ;set X for the next scan line
|
||||
add cx,dx ;advance error term
|
||||
jle XMajorNoAdvance ;not time for X coord to advance one
|
||||
; extra
|
||||
add ax,[bp+AdvanceAmt] ;advance X coord one extra
|
||||
sub cx,[bp+Height] ;adjust error term back
|
||||
XMajorNoAdvance:
|
||||
dec bx ;count off this scan line
|
||||
jnz XMajorLoop
|
||||
jmp ScanEdgeDone
|
||||
align 2
|
||||
ToScanEdgeExit:
|
||||
jmp ScanEdgeExit
|
||||
align2
|
||||
IsVertical:
|
||||
mov ax,[bp+X1] ;starting (and only) X coordinate
|
||||
sub bx,[bp+SkipFirst] ;loop count = Height - SkipFirst
|
||||
jz ScanEdgeExit ;no scan lines left after skipping 1st
|
||||
VerticalLoop:
|
||||
mov [di],ax ;store the current X value
|
||||
add di,size HLine ;point to the next HLine struc
|
||||
dec bx ;count off this scan line
|
||||
jnz VerticalLoop
|
||||
jmp ScanEdgeDone
|
||||
align 2
|
||||
IsDiagonal:
|
||||
mov ax,[bp+X1] ;starting X coordinate
|
||||
cmp [bp+SkipFirst],1 ;skip the first point?
|
||||
jz DiagonalSkipEntry ;yes
|
||||
DiagonalLoop:
|
||||
mov [di],ax ;store the current X value
|
||||
add di,size HLine ;point to the next HLine struc
|
||||
DiagonalSkipEntry:
|
||||
add ax,dx ;advance the X coordinate
|
||||
dec bx ;count off this scan line
|
||||
jnz DiagonalLoop
|
||||
jmp ScanEdgeDone
|
||||
align 2
|
||||
YMajor:
|
||||
pushbp ;preserve stack frame pointer
|
||||
mov si,[bp+X1] ;starting X coordinate
|
||||
cmp [bp+SkipFirst],1 ;skip the first point?
|
||||
mov bp,bx ;put Height in BP for error term calcs
|
||||
jz YMajorSkipEntry ;yes, skip the first point
|
||||
YMajorLoop:
|
||||
mov [di],si ;store the current X value
|
||||
add di,size HLine ;point to the next HLine struc
|
||||
YMajorSkipEntry:
|
||||
add cx,ax ;advance the error term
|
||||
jle YMajorNoAdvance ;not time for X coord to advance
|
||||
add si,dx ;advance the X coordinate
|
||||
sub cx,bp ;adjust error term back
|
||||
YMajorNoAdvance:
|
||||
dec bx ;count off this scan line
|
||||
jnz YMajorLoop
|
||||
popbp ;restore stack frame pointer
|
||||
ScanEdgeDone:
|
||||
cmp [bp+SetXStart],1 ;were we working with XStart field?
|
||||
jz UpdateHLinePtr ;yes, DI points to the next XStart
|
||||
sub di,XEnd ;no, point back to the XStart field
|
||||
UpdateHLinePtr:
|
||||
mov bx,[bp+EdgePointPtr] ;point to pointer to HLine array
|
||||
mov [bx],di ;update caller's HLine array pointer
|
||||
ScanEdgeExit:
|
||||
pop di ;restore caller's register variables
|
||||
pop si
|
||||
mov sp,bp ;deallocate local variables
|
||||
pop bp ;restore caller's stack frame
|
||||
ret
|
||||
_ScanEdge endp
|
||||
end
|
||||
```
|
||||
Loading…
Reference in a new issue