; tinyfrac.asm
; 
; teeny program displays the Mandelbrot set.
;
; This program was based on a program by Frank Hommers, later optimized
; for size by Mikko Hyvarinen and posted in Fidonet's 80XXX echo.
;
; pan     using the arrow keys, one can navigate the fractal.
;
;               Home  Up  PgUp
;               Left      Right   correspond to 8 obvious directions
;               End   Dn  PgDn
;
; zoom    there are now ten levels of magnification available.  If the
;         program is assembled with FEATURES defined, the number
;         corresponding to the zoom level (0-9, zero is most zoomed in)
;         is displayed in the upper left hand corner of the screen just
;         before each new fractal is drawn.  The gray '+' key zooms out,
;         the gray '-' key zooms in.
;
; beep    the program will beep at the completion of each fractal
;         drawing or if the user attempts to zoom past either limit.
;
; mode    if the program is assembled with MODECHANGE defined, the
;         will change to the next video mode if the 'v' key is pressed.
;         This is handy because drawing fractals at high resolution can
;         be very timeconsuming.  The user can find an interesting spot
;         in a low res mode and then change to a high res mode to see it
;         more fully rendered.
;
; size    this whole project was started off as a size optimization
;         exercise, so there have been some rather ugly tradeoffs to
;         sacrifice speed for size.
;
; 8086    yes, it runs on an 8086 although only if you leave out either
;         the FEATURES option or the MODECHANGE option and it would be
;         slower and more painful than oral surgery.
;
;
; to assemble & link:
;   TASM /m2 tinyfrac       (assemble using two pass mode if required)
;   TLINK /Tdc tinyfrac     (link Target platform is DOS, COM file)
;
;
        IDEAL
        MODEL TINY
        JUMPS           ; without this, see caveat under 8086 above 

NONE        = 00h       ; use this for no features
PRINTZOOM   = 01h       ; printout and beep features
MODECHANGE  = 02h       ; support video mode change?
SPEED       = 04h       ; use 386 instructions for speed
STARTCOORDS = 08h       ; use starting coordinates (instead of 0,0)
HIRES       = 10h       ; use hi resolution (single mode version only)

; choose the desired features from the feature list above, and OR them
; all together as shown below:

FEATURES    = PRINTZOOM OR MODECHANGE OR STARTCOORDS OR SPEED


if (FEATURES AND SPEED)
        P386
endif

ifdef HIRES
  VIDMODE   = 12h       ; use mode 12h
  PIXWIDTH  = 640       ; ... which is 640x480
  PIXHEIGHT = 480
else
  VIDMODE   = 13h       ; use mode 13h
  PIXWIDTH  = 320       ; ... which is 320x200
  PIXHEIGHT = 200
endif
TEXTMODE    =   3       ; our exit video mode (80x25 color text mode)
ZOOMLIMIT   =  13       ; can change to up to 13 for extended zoom in

VIDEO_INT   = 10h       ; BIOS video services interrupt
 WRITE_PIXEL = 0Ch      ; write pixel video service
 WRITE_CHAR  = 0eh      ; write char in TTY mode video service
 CHANGE_MODE = 00h      ; change mode video service

KEYBD_INT   = 16h       ; BIOS keyboard services interrupt

; ASCII codes
EXTENDED    = 000h      ; no ASCII code for extended key codes
BELL        = 007h      ; the ASCII bell char to make a beep
CR          = 00dh      ; a carriage return character
ESCAPE      = 01bh      ; the escape key
PLUS        = 02bh      ; ASCII code for '+' key
V_KEY       = 'v'       ; ASCII code for video mode switch

; keyboard scan codes
MINUS       = 04ah      ; scan code for gray '-' key

; feel free to experiment with the following constants:

DELTA       = 100       ; the unit of pan movement in pixels
THRESHOLD   =   4       ; must be in the range of (0,255)
STARTSCALE  =   7       ; a number from 0 to ZOOMLIMIT, inclusive
STARTX      =-DELTA     ; to the right by 1 delta unit (STARTCOORDS feature)
STARTY      =-DELTA     ; down by 1 delta unit (STARTCOORDS feature)
CHAR_COLOR  = 0fh       ; white on black background (for PRINTZOOM feature)

        codeseg
        org 100h
;****************************************************************************
;
;       Here's the main routine, and it's a bit convoluted.
;
;****************************************************************************
proc Start
ife (FEATURES AND MODECHANGE)
        mov     ax,VIDMODE
        int     VIDEO_INT
endif
if (FEATURES AND STARTCOORDS)
        mov     bp,STARTX
        mov     di,STARTY
else
        xor     bp,bp           ; zero initial X offset
        xor     di,di           ; initial Y offset is identical
endif
if   (FEATURES AND MODECHANGE)
        mov     si,offset VidTbl; point to default video table
        jmp     @@ChgMode

STRUC video
ScrnMode        dw      ?       ; the mode number for BIOS' purposes
ScrnWidth       dw      ?       ; pixel width of screen minus one
ScrnHeight      dw      ?       ; full height of screen in pixels
NextMode        dw      ?       ; pointer to next video structure
ENDS

;
;  video modes may be added by simply cloning one of the lines
;  which end with ($+2).  The table is a circular linked list, the
;  second entry of which will be the one initially used and the last
;  of which must point to the first entry.  I've added mode 54h by
;  way of example, but note that this mode is not supported by all video
;  hardware.  Check your video card's manual for supported modes and
;  resolutions.  Choose graphics modes with 16 or more colors for the
;  best effects.
;

VidTbl \
       video <54h, 800-1, 600, ($ + 2)>         ; highest res
       video <13h, 320-1, 200, ($ + 2)>         ; lowest res
       video <12h, 640-1, 480, offset VidTbl>   ; next to lowest res

else
        jmp     @@Render        ; leap right in there and draw
endif
@@TryPlus:
        cmp     al,PLUS         ; Q: gray + key?
        mov     al,[scale]      ; get the scale factor in al now
        jnz     @@TryMinus      ; N: maybe it's something else
        dec     al              ; Y: it's plus so zoom out
        js      @@beep          ; if AL<0, balk - can't zoom that far
        sar     bp,1            ; adjust offsets for new scale so
        sar     di,1            ; we stay in the same place
        jmp     @@AdjustScale
@@TryMinus:
        cmp     ah,MINUS        ; Q: gray - key?
        jnz     @@ReadKey       ; N: it's not a valid key
        inc     al              ; Y: zoom in
        cmp     al,ZOOMLIMIT    ; Q: have we zoomed too far?
        ja      @@beep          ; Y: yes, so just beep and don't adjust
        sal     bp,1            ; adjust offsets for new scale so
        sal     di,1            ; we stay in the same place

@@AdjustScale:
        mov     [scale],al      ; update the scale value
@@Render:
if (FEATURES AND PRINTZOOM)
        mov     al,'0'+ZOOMLIMIT; maximum printable character
        sub     al,[scale]      ; invert the sense
        call    PrintChar       ; show the character
        mov     al,CR           ; print a carriage return (no line feed -
        call    PrintChar       ;  we don't want to advance to next line)
endif
;****************************************************************************
;                                                                     Draw
;       This routine is the fractal drawing engine.  It has been
;       optimized for size, sacrificing speed.
;
;****************************************************************************
if (FEATURES AND MODECHANGE)
        mov     cx,[(video ptr si).ScrnHeight]
        push    si              ; we do this because it's very slow
                                ; if we read the Width from memory
                                ; every inner loop iteration
        mov     si,[(video ptr si).ScrnWidth]
else
        mov     cx, PIXHEIGHT   ; height of screen in pixels
endif
        sub     di,cx           ; adjust our Y offset
@@CalcRow:
        push    cx              ; save the row pointer on the stack
if (FEATURES AND MODECHANGE)
        mov     cx,si           ; fetch the screen width
else
        mov     cx, PIXWIDTH-1  ; width of screen in pixels
endif
        sub     bp,cx           ;
@@CalcPixel:
        push    cx              ; save the column counter on stack
        xor     cx, cx          ; clear out color loop counter
        xor     bx, bx          ; zero i coefficient
        xor     dx, dx          ; zero j coefficient
@@CycleColors:
        push    dx              ; save j value for later
        mov     ax, bx          ; ax = i
        sub     ax, dx          ; ax = i - j
        add     dx, bx          ; dx = i + j
        stc                     ; one additional shift, please
        call    Shifty          ; ax = ((i+j)*(i-j)) shifted right
        pop     dx              ; retrieve our saved value for j
        add     ax,bp           ; account for base offset...
        cmp     ah,THRESHOLD    ; Q: is i > THRESHOLD * 256?
        xchg    bx,ax           ; now swap new i with old i
        jg      @@draw          ; Y: draw this pixel
        clc                     ; no additional shifts here, please
        call    Shifty          ; now dx:ax = old i * j
        xchg    dx,ax           ;
        add     dx,di           ; account for base offset...
        inc     cl              ; increment color
        jnz     @@CycleColors   ; keep going until we're done
@@draw:
        xchg    ax, cx          ; mov color into al
        pop     cx              ; retrieve our column counter
        pop     dx              ; fetch row (column already in cx)
        push    dx              ; must leave a copy on the stack
        xor     bx,bx           ; write to video page zero
        mov     ah,WRITE_PIXEL  ; write pixel command
        int     VIDEO_INT       ; video BIOS call
        inc     bp              ; adjust our X base value
        loop    @@CalcPixel     ; keep going until we've done a line
        inc     di              ; adjust our Y base value
        pop     cx              ; keep going until we've done 'em all
        loop    @@CalcRow       ; more rows?

if (FEATURES AND MODECHANGE)
        pop     si              ; restore vid ptr if we use one
endif
@@beep:
if (FEATURES AND PRINTZOOM)
        mov     al,BELL         ;
        call    PrintChar       ;
else
        mov     ax,((WRITE_CHAR SHL 8) OR BELL)  ; make a beep
        int     VIDEO_INT       ; (bx=0 -- any video page, any char attr)
endif
@@ReadKey:
        xor     ax,ax           ; fetch a keystroke
        int     KEYBD_INT       ; keyboard request
        cmp     al,ESCAPE       ; Q: does the user want to exit?
        jz      @@exit          ; Y: do so immediately
if (FEATURES AND MODECHANGE)
        cmp     al,V_KEY        ; request for video mode change?
        jnz     @@TestExt       ; if not, go on
@@ChgMode:
        mov     si,[(video PTR si).NextMode]  ; change pointers
        mov     ax,[(video PTR si).ScrnMode]  ; load new video mode
        int     VIDEO_INT       ; change modes
        jmp     @@Render        ; draw new screen
@@TestExt:
endif
        cmp     al,EXTENDED     ; Q: is it an extended key code?
        jnz     @@TryPlus       ; N: it's not so see if it's '+'
@@ArrowKey:
        inc     ah              ; increment it to make indexing easier
        add     ah,ah           ; multiply by two
        mov     bl,6            ; fix template (bh is already zero)
        and     bl,ah           ; now bx contains address of delta
if (FEATURES AND MODECHANGE)
        push    si              ; save video ptr if we're using one
endif
        mov     si,offset Deltas; fetch the delta value
        add     bp,[bx+si]      ; add it to the X offset
        shr     ah,2            ; now look at the Y value of keystroke
        mov     bl,6            ; turn it into a table offset
        and     bl,ah           ; do it now
        sub     di,[bx+si]      ; and subtract from Y offset
if (FEATURES AND MODECHANGE)
        pop     si              ; restore video ptr if we're using one
endif
        jmp     @@Render        ; go draw this thing.
@@exit:
        mov     ax,TEXTMODE     ; back to normal now
        int     VIDEO_INT       ; change modes
        ret                     ; and exit via old style
endp Start

Deltas   dw +DELTA,0,-DELTA,0   ; handy table for calculating
                                ; changes in X and Y offsets

;****************************************************************************
;                                                                   Shifty
;
;       This routine multiplies AX by DX and shifts the result (in
;       DX:AX) to the right by scale bits (or scale+1 bits if CY is
;       set).  The resulting value is left in AX.  DX is destroyed.
;
;****************************************************************************
proc Shifty near
        push    cx              ; save middle bits (i*i - j*j)
        db      0b1h            ; code for mov cl,immed8
scale   db      STARTSCALE
        adc     cl,0            ; adjust per CY flag
        imul    dx              ; do the multiply
if (@Cpu AND 8)                 ; is is a 386 or better?
        xchg    ax,dx           ;
        shl     eax,16          ; put hi part in hi 16 bits
        xchg    ax,dx
        shr     eax,cl          ;
else
@@Rotate:
        rcr     dx,1            ;
        rcr     ax,1            ;
        loop    @@Rotate        ; ch is always zero so this is OK
endif
        pop     cx              ;
        ret                     ;
endp  Shifty

if (FEATURES AND PRINTZOOM)
;****************************************************************************
;                                                                PrintChar
;
;       This simple subroutine prints a single character (in AL) to the
;       screen using a BIOS call.  AH and BX are destroyed.
;
;****************************************************************************
proc PrintChar
        mov     ah,WRITE_CHAR   ; write a character in TTY mode
        mov     bx,CHAR_COLOR AND 07fh  ; use page 0 (bh), non-xor color (bl)
        int     VIDEO_INT       ; do it up
        ret
endp PrintChar
endif

        ends
        end Start

