;; Global variables used here ...
EVEN
ScrollPosX      dw      0       ; Scroll origin, upper-left X
ScrollPosY      dw      0       ; Scroll origin, upper-left Y
ScrollDX        dw      0       ; Amount to change scroll origin, X
ScrollDY        dw      0       ; Amount to change scroll origin, Y

;; SCROLL:
;; This routine takes care of all of the scrolling, however it calls
;; outside drawing routines to update the screen.  Scrollx and
;; Scrolly determine the amount to scroll by.
;; Note that this does only RELATIVE scrolling, not absolute scrolling.
;; Scroll saves time by updating only up to the one row or column of
;; tiles which have come into view due to a change in scroll offset.
;; In other words, it's not good for "jumping" to a particular point,
;; although this effect can be accomplished in other ways -- the draw_full
;; routine is available to draw a full screen again.
;; Sometimes this means that you will have to calculate values ahead of
;; time, for instance if you wish the scrolling to keep a certain sprite
;; in the center of the screen.  In this case, just set ScrollDX and
;; ScrollDY to the delta-x and delta-y of the sprite.
;; * Newly added:
;; Since there are three pages, it is necessary to keep each one of them
;; up to date with each scroll.  Recently, I was doing some fast (8+
;; pixels per frame) scrolling and noticed that there was a significant
;; pause when the screen snapped to a new origin.  (The origin is always
;; at a square's corner, even though it may not look like it because it
;; disguises things by smooth-panning the hardware.)  Every time it
;; scrolled, it was drawing the new information and copying it to the
;; two other planes.  I've now distributed the load over successive
;; pages, in other words it doesn't copy the new info all at once, but
;; over several frames.  This really smoothed out the scrolling so that
;; while there are still some jumps, they only occur very infrequently
;; and then only at 15 or 16 pixel/frame scroll rates...)  That's the
;; "catchup" code at the bottom, and that's why it's more complex than
;; it maybe could be...
EVEN
Scroll          PROC    near
        ; Using the ScrollDX variable as delta-x, move the scroll-origin
        ; in the x direction.  Then, if the visible screen is now
        ; viewing invalid data, snap the origin to a new point and
        ; draw any new columns that are necessary.
do_x_scroll:    mov     ax,cs:ScrollPosX
                add     ax,cs:ScrollDX           ; ScrollDX is a delta-x
                jl      wrap_l                  ; wrap left if negative
                cmp     ax,VIRTUAL_WIDTH - SCREEN_WIDTH ; too far right?
                jge     wrap_r                  ; wrap right if too big
                mov     cs:ScrollPosX,ax        ; Stores new scroll-x
        ; (just like above, for y:)
        ; Using the ScrollDY variable as delta-y, move the scroll-origin
        ; in the y direction.  Then, if the visible screen is now
        ; viewing invalid data, snap the origin to a new point and
        ; draw any new rows that are necessary.
do_y_scroll:    mov     ax,cs:ScrollPosY
                add     ax,cs:ScrollDY          ; ScrollDY is a delta-y
                jl      wrap_t                  ; wrap top if negative
                cmp     ax,(VIRTUAL_HEIGHT - SCREEN_HEIGHT) * VIRTUAL_WIDTH
                jge     wrap_b                  ; wrap bottom if too big
                mov     cs:ScrollPosY,ax        ; Store the new scroll-y
                jmp     calculate

        ; To wrap to the right:
        ; Add a square's width to the origin's upper left corner, and
        ; subtract the same amount from the scroll origin's upper left
        ; corner.  This makes no difference on the screen but allows
        ; us to forget about the leftmost column on the screen (it's
        ; offscreen now...) so we can take over the right column.
        ; See any documentation I included for an explanation of the
EVEN    ; scrolling...
wrap_r:         add     cs:upper_left,SQUARE_WIDTH / 4
                sub     ax,SQUARE_WIDTH
                mov     cs:ScrollPosX,ax

                mov     dx,MapInfo.Wid
                mov     bp,MapInfo.OffX1
                inc     bp
                cmp     bp,dx
                jb      wrap_r1_ok
                sub     bp,dx
wrap_r1_ok:     mov     MapInfo.OffX1,bp

                mov     bp,MapInfo.OffX2
                inc     bp
                cmp     bp,dx
                jb      wrap_r2_ok
                sub     bp,dx
wrap_r2_ok:     mov     MapInfo.OffX2,bp

                mov     bp,MapInfo.WrapX
                dec     bp
                jnz     wrap_r3_ok
                add     bp,dx
wrap_r3_ok:     mov     MapInfo.WrapX,bp

                call    update_right
                jmp     do_y_scroll     ; Jump back to do Y

EVEN    ; Same for left side
wrap_l:         sub     cs:upper_left,SQUARE_WIDTH / 4
                add     ax,SQUARE_WIDTH
                mov     cs:ScrollPosX,ax

                mov     dx,MapInfo.Wid
                mov     bp,MapInfo.OffX1
                dec     bp
                cmp     bp,dx
                jb      wrap_l1_ok
                add     bp,dx
wrap_l1_ok:     mov     MapInfo.OffX1,bp

                mov     bp,MapInfo.OffX2
                dec     bp
                cmp     bp,dx
                jb      wrap_l2_ok
                add     bp,dx
wrap_l2_ok:     mov     MapInfo.OffX2,bp

                mov     bp,MapInfo.WrapX
                inc     bp
                cmp     bp,dx
                jbe     wrap_l3_ok
                sub     bp,dx
wrap_l3_ok:     mov     MapInfo.WrapX,bp

                call    update_left
                jmp     do_y_scroll     ; Jump back to do Y

EVEN    ; Same for bottom
wrap_b:         add     cs:upper_left,(SQUARE_HEIGHT * VIRTUAL_WIDTH) / 4
                sub     ax,SQUARE_HEIGHT * VIRTUAL_WIDTH
                mov     cs:ScrollPosY,ax

                mov     bp,MapInfo.OffY1
                mov     dx,MapInfo.Extent
                add     bp,MapInfo.Wid
                cmp     bp,dx
                jb      wrap_b1_ok
                sub     bp,dx
wrap_b1_ok:     mov     MapInfo.OffY1,bp

                mov     bp,MapInfo.OffY2
                add     bp,MapInfo.Wid
                cmp     bp,dx
                jb      wrap_b2_ok
                sub     bp,dx
wrap_b2_ok:     mov     MapInfo.OffY2,bp

                mov     dx,MapInfo.Ht
                mov     bp,MapInfo.WrapY
                dec     bp
                jg      wrap_b3_ok
                add     bp,dx
wrap_b3_ok:     mov     MapInfo.WrapY,bp

                call    update_bottom
                mov     ax,cs:ScrollPosY
                jmp     calculate       ; Jump down to calc new offsets

EVEN    ; Same for top
wrap_t:         sub     cs:upper_left,(SQUARE_HEIGHT * VIRTUAL_WIDTH) / 4
                add     ax,SQUARE_HEIGHT * VIRTUAL_WIDTH
                mov     cs:ScrollPosY,ax

                mov     bp,MapInfo.OffY1
                mov     dx,MapInfo.Extent
                sub     bp,MapInfo.Wid
                cmp     bp,dx
                jb      wrap_t1_ok
                add     bp,dx
wrap_t1_ok:     mov     MapInfo.OffY1,bp

                mov     bp,MapInfo.OffY2
                sub     bp,MapInfo.Wid
                cmp     bp,dx
                jb      wrap_t2_ok
                add     bp,dx
wrap_t2_ok:     mov     MapInfo.OffY2,bp

                mov     bp,MapInfo.WrapY
                mov     dx,MapInfo.Ht
                inc     bp
                cmp     bp,dx
                jbe     wrap_t3_ok
                sub     bp,dx
wrap_t3_ok:     mov     MapInfo.WrapY,bp

                call    update_top
                mov     ax,cs:ScrollPosY
                jmp     calculate       ; Jump down to calc new offsets

EVEN
align_mask_table DB     11h,22h,44h,88h
calculate:
        ; Calculate the scroll offset
        ; AX already = ScrollPosY
                add     ax,cs:ScrollPosX        ;Now AX = scroll offset

        ; Calculate the plane alignment
                mov     bl,al
                and     bx,0003h
                mov     cs:DrawPage.Alignment,bl
;               mov     bl,cs:align_mask_table[bx]
;               mov     cs:DrawPage.AlignmentMask,bl

        ; Now we don't need Scroll Offset on a pixel level any more,
        ; so shift it to a byte level (/4) and store it away.
                shr     ax,2
                mov     cs:DrawPage.ScrollOffset,ax

        ; Calculate the actual upper left corner address
                mov     si,cs:DrawPage.Address
                add     si,cs:upper_left
                mov     cs:DrawPage.UpperLeftAddress,si

        ; And the map offset:
                mov     bx,MapInfo.WrapX
                mov     cs:DrawPage.MapPosX,bx
                mov     di,MapInfo.WrapY
                mov     cs:DrawPage.MapPosY,di

                mov     cs:DrawPage.Valid,1
                cmp     cs:BlankPage.Valid,0
                je      no_catch_up

        ; Lastly, update dirty area (if any) on blank page.
        ; BP still contains the draw page's mapoffset.
                sub     bx,cs:BlankPage.MapPosX
                sub     di,cs:BlankPage.MapPosY
                jnz     yes_catch_up
                cmp     bx,0
                jnz     yes_catch_up
        ; No catchup necessary -- return.
no_catch_up:    ret

;; Okay, this stuff is a mess.  I've registerized everything except
;; for the video data itself.  I'll try to comment it best I can.
EVEN
yes_catch_up:
        ; First, switch into full-copy mode.  This means latching the
        ; bit mask as coming entirely from the local 32-bit registers
        ; and then setting the map mask to write to all 4 planes.  This
        ; is Mode X's greatest advantage, when you can do it!  It
        ; provides a 2x speedup or so...
                mov     dx,SC_INDEX     ; Select Sequencer input
                mov     ax,0F02h
                out     dx,ax           ; set map mask = all bits

                mov     dx,GC_INDEX
                mov     ax,ALL_COPY_BITS
                out     dx,ax

                JKEYNP  kB,isntbp
isbp:           nop
isntbp:
        ; Next, calculate the amount to catch up the top/bottom rows
        ; If we just wrapped over the edge, it is possible that the
        ; distance traveled will be as high as MapInfo.Ht - 1.  So,
        ; in the fashion of signed numbers, if the number is greater
        ; than MapInfo.Ht / 2, we take it to mean negative.  To convert
        ; it to signed, we have to shift it into the proper range.  But
        ; if it's less than MapInfo.Ht / 2, then it's okay as it is.
                mov     ax,di
                cmp     ax,0
                je      y_mod

                mov     cx,MapInfo.Ht
                cwd             ; DX = -1 or 0 based on AX's sign.
                and     dx,cx   ; DX = Ht or 0
                add     ax,dx   ; AX = 0 ... Ht (unsigned)

                mov     di,ax
                shl     di,1
                cmp     di,cx
                jb      y_signed
                sub     ax,cx
y_signed:       neg     ax

        ; Find DI MOD MapInfo.Wid, and then convert to it into virtual
        ; coordinates from map offset coordinates.
        ; This routine also calculates BP, which will be used as a loop
        ; counter to determine how many rows to draw on the left/right
        ; column copy.
y_mod:          mov     bp,ax
                cwd
                add     bp,dx
                xor     bp,dx
                shl     bp,3            ; BP = (SQUARE_HEIGHT / 2) * dX
                mov     di,cs:MultVirtWidth[bp] ; Use multiplication table
                add     di,dx                   ; to calculate new DI, then
                xor     di,dx                   ; restore the sign.
                sub     bp,VIRTUAL_HEIGHT / 2
        ; Out:  DI = # of pixels traveled,
        ;       BP = (VIRTUAL_HEIGHT - # of rows) / 2

        ; Change BX (delta-x) to signed from unsigned, store in AX
                mov     ax,bx
                mov     cx,MapInfo.Wid
                cwd
                and     dx,cx   ; DX = Wid or 0
                add     ax,dx   ; AX = 0 ... Wid

                mov     bx,ax
                shl     bx,1
                cmp     bx,cx
                jb      x_signed
                sub     ax,cx
x_signed:

        ; The following is an optimization which would slow down on
        ; normal memory, but I believe it will be okay on VGA memory,
        ; which is so incredibly slow.  Basically, I've replaced all
        ; "rep movsb"'s with a loop that first calculates "bx = di - si",
        ; and then loops performing "mov ds:[si],es:[si+bx]".  Why?
        ; Because of several reasons, none of which I'm sure actually
        ; help out, but they do make for smaller code.  1)  It means that
        ; I only have to maintain SI, and "DI" is maintained automatically
        ; (because DI - SI should remain constant).  2)  Don't have to
        ; calculate DS.  Not much gain here.  3)  Because I'd already
        ; unrolled the loops, and the "rep movsb"'s had become instead
        ; "mov al, ds:[si] / mov es:[di], al / mov al, ds:[si + 1] /
        ; mov es:[di + 1],al ... etc ... add si, 4 / add di, 4".  In
        ; other words, I wasn't using MOVSB anyway.  The only advantage
        ; I can see in MOVSB is that it doesn't have to store the answer
        ; in AL so it could be slightly faster.  By unrolling the loops,
        ; I'd already made up for that, I think.  4)  Normally, using
        ; [SI + BX + 1] would incur a penalty of an additional clock
        ; cycle (because it has to add two indexs + an offset).  But
        ; the VGA memory and the '86 CPU can multi-task, and the VGA
        ; is very slow.  So by the time the VGA is ready to write the
        ; next byte, the one extra clock cycle has already passed.
        ;
        ; Am I right?  Does this make things faster?  I have no idea.
        ; I haven't bothered to check both ways.  Please let me know
        ; if I've missed something important...
        ;
        ; Here's the calculation of BX.  SI is already set.
                ; si already = DrawPage.UpperLeftAddress
                mov     bx,cs:BlankPage.Address
                sub     bx,cs:DrawPage.Address

        ; Now, converts SI into "1/4" units.  I do all the calculations
        ; in "1/4" scale and then scale back up, mostly because it saved
        ; me some instructions elsewhere.
                shr     si,2
        ; Stores this value of SI.  This will be restored after doing
        ; the top/bottom copying.
                mov     dx,si

        ; Check if it's necessary to catch up the top or bottom.
catchup_tb:     cmp     di,0
                je      catchup_tb_end
                jl      catchup_t
catchup_b:      ; COPY BOTTOM
        ; Move SI to point at the bottom of the screen - # of rows
        ; to update.
                add     si,((VIRTUAL_WIDTH * VIRTUAL_HEIGHT) / 4) / 4
                sub     si,di
                jmp     copy_tb
catchup_t:      ; COPY_TOP
        ; Leave SI, but add to the "pushed" value of SI the number of
        ; rows that will be drawn.  This prevents overlap between top
        ; and right/left when moving diagonally.  Also, DI = |DI|
                neg     di
                add     dx,di

        ; Now do the actual copying.  Shifts SI back into scale "1",
        ; then performs an unrolled loop to copy the entire virtual
        ; width * # of pixel rows.  Since DI is already in "1/4" scale,
        ; it is only decremented once for each four pixels drawn.
copy_tb:        shl     si,2
copy_tb_loop:   mov     cl,es:[si]
                mov     es:[si+bx],cl
                mov     cl,es:[si+1]
                mov     es:[si+bx+1],cl
                mov     cl,es:[si+2]
                mov     es:[si+bx+2],cl
                mov     cl,es:[si+3]
                mov     es:[si+bx+3],cl
                add     si,4
                dec     di
                jnz     copy_tb_loop
catchup_tb_end:

        ; Next, check to see if it's necessary to draw the right or
        ; the left side.
catchup_rl:     cmp     ax,0
                je      catchup_rl_end
                jg      catchup_l
catchup_r:      ; COPY RIGHT
        ; Adds to the "pushed" SI the width of the screen, minus
        ; the number of rows to be drawn.
                neg     ax
                add     dx,(VIRTUAL_WIDTH / 4) / 4
                sub     dx,ax
catchup_l:      ; COPY LEFT (or nothing)

        ; Does the actual copying.  First pops SI from its stored value
        ; and shifts it back into scale "1"
copy_rl:        mov     si,dx
                shl     si,2

        ; This is a loop over BP -- which has already been set as
        ; VIRTUAL_HEIGHT - (# of bytes drawn in vertical update)
        ; Again, this loop is unrolled such that it does two rows @
        ; 4 bytes each with every iteration.
        ; This LEA instruction is just a quick MOV DI, SI + 2 *y
        ; DI is used to push the next value of SI for each iteration
        ; of the loop.
copy_rl_loop:   lea     di,[si + 2*(VIRTUAL_WIDTH/4)]
                mov     cx,ax
copy_rl_col:    mov     dl,es:[si]
                mov     es:[si+bx],dl
                mov     dl,es:[si+1]
                mov     es:[si+bx+1],dl
                mov     dl,es:[si+2]
                mov     es:[si+bx+2],dl
                mov     dl,es:[si+3]
                mov     es:[si+bx+3],dl
                mov     dl,es:[si+VIRTUAL_WIDTH/4]
                mov     es:[si+bx+VIRTUAL_WIDTH/4],dl
                mov     dl,es:[si+VIRTUAL_WIDTH/4+1]
                mov     es:[si+bx+VIRTUAL_WIDTH/4+1],dl
                mov     dl,es:[si+VIRTUAL_WIDTH/4+2]
                mov     es:[si+bx+VIRTUAL_WIDTH/4+2],dl
                mov     dl,es:[si+VIRTUAL_WIDTH/4+3]
                mov     es:[si+bx+VIRTUAL_WIDTH/4+3],dl
                add     si,4
                dec     cx
                jnz     copy_rl_col
                mov     si,di           ; SI = pop (SI + VIRTUAL_WIDTH/4)
                inc     bp              ; (BP is negative, so INC it)
                jnz     copy_rl_loop
catchup_rl_end:

        ; Switch back to all-draw mode.
                mov     dx,GC_INDEX
                mov     ax,ALL_DRAW_BITS
                out     dx,ax
                ret
Scroll          ENDP
