401 lines
17 KiB
Markdown
401 lines
17 KiB
Markdown
**LISTING 35.3 L35-3.ASM**
|
|
|
|
; Fast assembler implementation of Bresenham's line-drawing algorithm
|
|
; for the EGA and VGA. Works in modes 0Eh, 0Fh, 10h, and 12h.
|
|
; Borland C++ near-callable.
|
|
; Bit mask accumulation technique when |DeltaX| >= |DeltaY|
|
|
; suggested by Jim Mackraz.
|
|
;
|
|
; Assembled with TASM
|
|
;
|
|
; By Michael Abrash
|
|
;
|
|
;****************************************************************
|
|
; C-compatible line-drawing entry point at _EVGALine. *
|
|
; Near C-callable as: *
|
|
; EVGALine(X0, Y0, X1, Y1, Color); *
|
|
;****************************************************************
|
|
;
|
|
|
|
model small
|
|
.code
|
|
|
|
;
|
|
; Equates.
|
|
;
|
|
EVGA_SCREEN_WIDTH_IN_BYTES equ 80 ;memory offset from start of
|
|
; one row to start of next
|
|
; in display memory
|
|
EVGA_SCREEN_SEGMENT equ 0a000h ;display memory segment
|
|
GC_INDEX equ 3ceh ;Graphics Controller
|
|
; Index register port
|
|
SET_RESET_INDEX equ 0 ;indexes of needed
|
|
ENABLE_SET_RESET_INDEX equ 1 ; Graphics Controller
|
|
BIT_MASK_INDEX equ 8 ; registers
|
|
|
|
;
|
|
; Stack frame.
|
|
;
|
|
EVGALineParms struc
|
|
dw ? ;pushed BP
|
|
dw ? ;pushed return address (make double
|
|
; word for far call)
|
|
X0 dw ? ;starting X coordinate of line
|
|
Y0 dw ? ;starting Y coordinate of line
|
|
X1 dw ? ;ending X coordinate of line
|
|
Y1 dw ? ;ending Y coordinate of line
|
|
Color db ? ;color of line
|
|
db ? ;dummy to pad to word size
|
|
EVGALineParms ends
|
|
|
|
;****************************************************************
|
|
; Line drawing macros. *
|
|
;****************************************************************
|
|
|
|
;
|
|
; Macro to loop through length of line, drawing each pixel in turn.
|
|
; Used for case of |DeltaX| >= |DeltaY|.
|
|
; Input:
|
|
; MOVE_LEFT: 1 if DeltaX < 0, 0 else
|
|
; AL: pixel mask for initial pixel
|
|
; BX: |DeltaX|
|
|
; DX: address of GC data register, with index register set to
|
|
; index of Bit Mask register
|
|
; SI: DeltaY
|
|
; ES:DI: display memory address of byte containing initial
|
|
; pixel
|
|
;
|
|
LINE1 macro MOVE_LEFT
|
|
local LineLoop, MoveXCoord, NextPixel, Line1End
|
|
local MoveToNextByte, ResetBitMaskAccumulator
|
|
mov cx,bx ;# of pixels in line
|
|
jcxz Line1End ;done if there are no more pixels
|
|
; (there's always at least the one pixel
|
|
; at the start location)
|
|
shl si,1 ;DeltaY * 2
|
|
mov bp,si ;error term
|
|
sub bp,bx ;error term starts at DeltaY * 2 - DeltaX
|
|
shl bx,1 ;DeltaX * 2
|
|
sub si,bx ;DeltaY * 2 - DeltaX * 2 (used in loop)
|
|
add bx,si ;DeltaY * 2 (used in loop)
|
|
mov ah,al ;set aside pixel mask for initial pixel
|
|
; with AL (the pixel mask accumulator) set
|
|
; for the initial pixel
|
|
LineLoop:
|
|
;
|
|
; See if it's time to advance the Y coordinate yet.
|
|
;
|
|
and bp,bp ;see if error term is negative
|
|
js MoveXCoord ;yes, stay at the same Y coordinate
|
|
;
|
|
; Advance the Y coordinate, first writing all pixels in the current
|
|
; byte, then move the pixel mask either left or right, depending
|
|
; on MOVE_LEFT.
|
|
;
|
|
out dx,al ;set up bit mask for pixels in this byte
|
|
xchg byte ptr [di],al
|
|
;load latches and write pixels, with bit mask
|
|
; preserving other latched bits. Because
|
|
; set/reset is enabled for all planes, the
|
|
; value written actually doesn't matter
|
|
add di,EVGA_SCREEN_WIDTH_IN_BYTES ;increment Y coordinate
|
|
add bp,si ;adjust error term back down
|
|
;
|
|
; Move pixel mask one pixel (either right or left, depending
|
|
; on MOVE_LEFT), adjusting display memory address when pixel mask wraps.
|
|
;
|
|
if MOVE_LEFT
|
|
rol ah,1 ;move pixel mask 1 pixel to the left
|
|
else
|
|
ror ah,1 ;move pixel mask 1 pixel to the right
|
|
endif
|
|
jnc ResetBitMaskAccumulator ;didn't wrap to next byte
|
|
jmp short MoveToNextByte ;did wrap to next byte
|
|
;
|
|
; Move pixel mask one pixel (either right or left, depending
|
|
; on MOVE_LEFT), adjusting display memory address and writing pixels
|
|
; in this byte when pixel mask wraps.
|
|
;
|
|
MoveXCoord:
|
|
add bp,bx ;increment error term & keep same
|
|
if MOVE_LEFT
|
|
rol ah,1 ;move pixel mask 1 pixel to the left
|
|
else
|
|
ror ah,1 ;move pixel mask 1 pixel to the right
|
|
endif
|
|
jnc NextPixel ;if still in same byte, no need to
|
|
; modify display memory yet
|
|
out dx,al ;set up bit mask for pixels in this byte.
|
|
xchg byte ptr [di],al
|
|
;load latches and write pixels, with bit mask
|
|
; preserving other latched bits. Because
|
|
; set/reset is enabled for all planes, the
|
|
; value written actually doesn't matter
|
|
MoveToNextByte:
|
|
if MOVE_LEFT
|
|
dec di ;next pixel is in byte to left
|
|
else
|
|
inc di ;next pixel is in byte to right
|
|
endif
|
|
ResetBitMaskAccumulator:
|
|
sub al,al ;reset pixel mask accumulator
|
|
NextPixel:
|
|
or al,ah ;add the next pixel to the pixel mask
|
|
; accumulator
|
|
loop LineLoop
|
|
;
|
|
; Write the pixels in the final byte.
|
|
;
|
|
Line1End:
|
|
out dx,al ;set up bit mask for pixels in this byte
|
|
xchg byte ptr [di],al
|
|
;load latches and write pixels, with bit mask
|
|
; preserving other latched bits. Because
|
|
; set/reset is enabled for all planes, the
|
|
; value written actually doesn't matter
|
|
endm
|
|
|
|
;
|
|
; Macro to loop through length of line, drawing each pixel in turn.
|
|
; Used for case of DeltaX < DeltaY.
|
|
; Input:
|
|
; MOVE_LEFT: 1 if DeltaX < 0, 0 else
|
|
; AL: pixel mask for initial pixel
|
|
; BX: |DeltaX|
|
|
; DX: address of GC data register, with index register set to
|
|
; index of Bit Mask register
|
|
; SI: DeltaY
|
|
; ES:DI: display memory address of byte containing initial
|
|
; pixel
|
|
;
|
|
LINE2 macro MOVE_LEFT
|
|
local LineLoop, MoveYCoord, ETermAction, Line2End
|
|
mov cx,si ;# of pixels in line
|
|
jcxz Line2End ;done if there are no more pixels
|
|
shl bx,1 ;DeltaX * 2
|
|
mov bp,bx ;error term
|
|
sub bp,si ;error term starts at DeltaX * 2 - DeltaY
|
|
shl si,1 ;DeltaY * 2
|
|
sub bx,si ;DeltaX * 2 - DeltaY * 2 (used in loop)
|
|
add si,bx ;DeltaX * 2 (used in loop)
|
|
;
|
|
; Set up initial bit mask & write initial pixel.
|
|
;
|
|
out dx,al
|
|
xchg byte ptr [di],ah
|
|
;load latches and write pixel, with bit mask
|
|
; preserving other latched bits. Because
|
|
; set/reset is enabled for all planes, the
|
|
; value written actually doesn't matter
|
|
LineLoop:
|
|
;
|
|
; See if it's time to advance the X coordinate yet.
|
|
;
|
|
and bp,bp ;see if error term is negative
|
|
jns ETermAction ;no, advance X coordinate
|
|
add bp,si ;increment error term & keep same
|
|
jmp short MoveYCoord ; X coordinate
|
|
ETermAction:
|
|
;
|
|
; Move pixel mask one pixel (either right or left, depending
|
|
; on MOVE_LEFT), adjusting display memory address when pixel mask wraps.
|
|
;
|
|
if MOVE_LEFT
|
|
rol al,1
|
|
sbb di,0
|
|
else
|
|
ror al,1
|
|
adc di,0
|
|
endif
|
|
out dx,al ;set new bit mask
|
|
add bp,bx ;adjust error term back down
|
|
;
|
|
; Advance Y coordinate.
|
|
;
|
|
MoveYCoord:
|
|
add di,EVGA_SCREEN_WIDTH_IN_BYTES
|
|
;
|
|
; Write the next pixel.
|
|
;
|
|
xchg byte ptr [di],ah
|
|
;load latches and write pixel, with bit mask
|
|
; preserving other latched bits. Because
|
|
; set/reset is enabled for all planes, the
|
|
; value written actually doesn't matter
|
|
;
|
|
loop LineLoop
|
|
Line2End:
|
|
endm
|
|
|
|
;****************************************************************
|
|
; Line drawing routine. *
|
|
;****************************************************************
|
|
|
|
public _EVGALine
|
|
_EVGALine proc near
|
|
push bp
|
|
mov bp,sp
|
|
push si ;preserve register variables
|
|
push di
|
|
push ds
|
|
;
|
|
; Point DS to display memory.
|
|
;
|
|
mov ax,EVGA_SCREEN_SEGMENT
|
|
mov ds,ax
|
|
;
|
|
; Set the Set/Reset and Set/Reset Enable registers for
|
|
; the selected color.
|
|
;
|
|
mov dx,GC_INDEX
|
|
mov al,SET_RESET_INDEX
|
|
out dx,al
|
|
inc dx
|
|
mov al,[bp+Color]
|
|
out dx,al
|
|
dec dx
|
|
mov al,ENABLE_SET_RESET_INDEX
|
|
out dx,al
|
|
inc dx
|
|
mov al,0ffh
|
|
out dx,al
|
|
;
|
|
; Get DeltaY.
|
|
;
|
|
mov si,[bp+Y1] ;line Y start
|
|
mov ax,[bp+Y0] ;line Y end, used later in
|
|
;calculating the start address
|
|
sub si,ax ;calculate DeltaY
|
|
jns CalcStartAddress ;if positive, we're set
|
|
;
|
|
; DeltaY is negative — swap coordinates so we're always working
|
|
; with a positive DeltaY.
|
|
;
|
|
mov ax,[bp+Y1] ;set line start to Y1, for use
|
|
; in calculating the start address
|
|
mov dx,[bp+X0]
|
|
xchg dx,[bp+X1]
|
|
mov [bp+X0],dx ;swap X coordinates
|
|
neg si ;convert to positive DeltaY
|
|
;
|
|
; Calculate the starting address in display memory of the line.
|
|
; Hardwired for a screen width of 80 bytes.
|
|
;
|
|
CalcStartAddress:
|
|
shl ax,1 ;Y0 * 2 ;Y0 is already in AX
|
|
shl ax,1 ;Y0 * 4
|
|
shl ax,1 ;Y0 * 8
|
|
shl ax,1 ;Y0 * 16
|
|
mov di,ax
|
|
shl ax,1 ;Y0 * 32
|
|
shl ax,1 ;Y0 * 64
|
|
add di,ax ;Y0 * 80
|
|
mov dx,[bp+X0]
|
|
mov cl,dl ;set aside lower 3 bits of column for
|
|
and cl,7 ; pixel masking
|
|
shr dx,1
|
|
shr dx,1
|
|
shr dx,1 ;get byte address of column (X0/8)
|
|
add di,dx ;offset of line start in display segment
|
|
;
|
|
; Set up GC Index register to point to the Bit Mask register.
|
|
;
|
|
mov dx,GC_INDEX
|
|
mov al,BIT_MASK_INDEX
|
|
out dx,al
|
|
inc dx ;leave DX pointing to the GC Data register
|
|
;
|
|
; Set up pixel mask (in-byte pixel address).
|
|
;
|
|
mov al,80h
|
|
shr al,cl
|
|
;
|
|
; Calculate DeltaX.
|
|
;
|
|
mov bx,[bp+X1]
|
|
sub bx,[bp+X0]
|
|
;
|
|
; Handle correct one of four octants.
|
|
;
|
|
js NegDeltaX
|
|
cmp bx,si
|
|
jb Octant1
|
|
;
|
|
; DeltaX >= DeltaY >= 0.
|
|
;
|
|
LINE1 0
|
|
jmp EVGALineDone
|
|
;
|
|
; DeltaY > DeltaX >= 0.
|
|
;
|
|
Octant1:
|
|
LINE2 0
|
|
jmp short EVGALineDone
|
|
;
|
|
NegDeltaX:
|
|
neg bx ;|DeltaX|
|
|
cmp bx,si
|
|
jb Octant2
|
|
;
|
|
; |DeltaX| >= DeltaY and DeltaX < 0.
|
|
;
|
|
LINE1 1
|
|
jmp short EVGALineDone
|
|
;
|
|
; |DeltaX| < DeltaY and DeltaX < 0.
|
|
;
|
|
Octant2:
|
|
LINE2 1
|
|
;
|
|
EVGALineDone:
|
|
;
|
|
; Restore EVGA state.
|
|
;
|
|
mov al,0ffh
|
|
out dx,al ;set Bit Mask register to 0ffh
|
|
dec dx
|
|
mov al,ENABLE_SET_RESET_INDEX
|
|
out dx,al
|
|
inc dx
|
|
sub al,al
|
|
out dx,al ;set Enable Set/Reset register to 0
|
|
;
|
|
pop ds
|
|
pop di
|
|
pop si
|
|
pop bp
|
|
ret
|
|
_EVGALine endp
|
|
|
|
end
|
|
|
|
An explanation of the workings of the code in Listing 35.3 would be a
|
|
lengthy one, and would be redundant since the basic operation of the
|
|
code in Listing 35.3 is no different from that of the code in Listing
|
|
35.1, although the implementation is much changed due to the nature of
|
|
assembly language and also due to designing for speed rather than for
|
|
clarity. Given that you thoroughly understand the C implementation in
|
|
Listing 35.1, the assembly language implementation in Listing 35.3,
|
|
which is well-commented, should speak for itself.
|
|
|
|
One point I do want to make is that Listing 35.3 incorporates a clever
|
|
notion for which credit is due Jim Mackraz, who described the notion in
|
|
a letter written in response to an article I wrote long ago in the late
|
|
and lamented *Programmer's Journal*. Jim's suggestion was that when
|
|
drawing lines for which |**DeltaX**| is greater than |**DeltaY**|, bits
|
|
set to 1 for each of the pixels controlled by a given byte can be
|
|
accumulated in a register, rather than drawing each pixel individually.
|
|
All the pixels controlled by that byte can then be drawn at once, with a
|
|
single access to display memory, when all pixel processing associated
|
|
with that byte has been completed. This approach can save many **OUT**s
|
|
and many display memory reads and writes when drawing nearly-horizontal
|
|
lines, and that's important because EGAs and VGAs hold the CPU up for a
|
|
considerable period of time on each I/O operation and display memory
|
|
access.
|
|
|
|
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?
|