;; 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