--- 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 #include #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. > ![](images/i.jpg) > 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 #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. > ![](images/i.jpg) > 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 ```