Scrolling Software en asm 6809

Introduction

Sur un Thomson MO/TO, le signal vidéo est transmis de manière progressive par un automate hardware qui effectue une lecture de la RAM vidéo durant le cycle inactif du 6809. Par conséquent, le seul moyen de faire défiler l’écran est de modifier le contenu de cette RAM.

Il n’y a pas possibilité de faire varier le point de départ de la lecture effectuée par l’automate : c’est dommage car cela aurait permis d’effectuer un scrolling hardware comme cela peut exister sur d’autres machines telles que le commodore 64, les Amstrad CPC, l’Amiga, l’Atari ST pour lesquelles il suffit de modifier un registre mémoire pour décaler l’affichage.

En combinant plusieurs techniques de programmation, on peut cependant réaliser un scrolling performant et 100% software par le processeur 6809.

Etape 1 : Stack Blast

Un moyen bien connu pour produire des graphismes rapides est celui des sprites compilés. Avec le processeur 6809, les sprites compilés sont très rapides s’ils sont combinés avec la technique du stack blast.

Le Stack Blast consiste à utiliser les instruction PULx/PSHx pour effectuer des lectures et écritures rapides en mémoire. Dans notre cas nous allons utiliser l’instruction PSHS pour écrire rapidement en mémoire vidéo. Le coût est seulement de 5 cycles pour l’instruction plus 1 cycle pour chaque octet écrit. Dans notre cas, il s’agira d’écrire 8 octets à chaque fois, soit 13 cycles consommés (5 + 8).

L’avantage de cette instruction est que le registre S qui stocke l’adresse de destination est mis à jour automatiquement en fonction du nombre d’octets écrits. On peut donc enchainer plusieurs PSHS sans avoir besoin de se soucier de la mise à jour de l’adresse de destination pour la prochaine instruction d’écriture.

Les registres D,X,Y,U vont nous permettre de stocker les données “pixels”. Ces données sont chargées dans les registres en mode immédiat, c’est le principe des sprites compilés.

Un “scroll-chunk”:

Opcode/post-bytes ASM Code Cycles
CC xx xx ldd #$[xx xx : pixels data 16 bits] 3 cycles
8E xx xx ldx #$[xx xx : pixels data 16 bits] 3 cycles
10 8E xx xx ldy #$[xx xx : pixels data 16 bits] 4 cycles
CE xx xx ldu #$[xx xx : pixels data 16 bits] 3 cycles
34 76 pshs u,y,x,d 13 cycles

Notez que LDY consomme 1 cycle de plus que les autres instructions de chargement de registre.

Un scroll chunk, qui représente alors 64 bits utiles, c’est à dire 16 pixels, occupe dont 15 octets en mémoires, c’est à dire 120 bits.

Sur un Thomson TO8 en mode BM16, en plaçant le registre S en fin de RAM vidéo, on peut ainsi peupler l’ensemble des données d’un écran 160x200 au moyen de seulement 2000 scroll-chunk comme celui ci dessus (8000 octets en Ram A et 8000 octets en Ram B) pour un total de 16000 octets et l’usage de 52000 cycles.

Attention: En utilisant le registre S on détourne l’usage du pointeur de la pile système, or chaque déclenchement d’IRQ écrit 12 octets en S (sauvegarde des registres processeur) à n’importe quel moment du programme. Lorsque la routine d’écriture ci dessus aura positionné S en limite de RAM vidéo un débordement pourra donc se produire en dehors de la zone utile de la RAM vidéo. En pratique sur un TO8, il faudra préserver les zones mémoire : 9FF4-9FFF et BFF4-BFFF qui se situent en amont des RAMA et RAM B (l’écriture par PSH se fait en remontant). Il est recommandé dans une IRQ utilisateur de repositionner la pile système S vers un espace tampon de manière à limiter ce débordement à 12 octets.

Etape 2: Loop unrolling

On pourrait imaginer utiliser des scroll-chunks comme éléments de tuile pour un système de tilemap, mais le code de contrôle serait trop consommateur de cycles. Afin de réduire au strict minimum le temps d’exécution de l’ensemble des scroll-chunk nécessaires au rafraichissement complet de la RAM vidéo, il faut dérouler le code (loop unrolling).

En déroulant l’ensemble du code nécessaire à la mise à jour de la RAMA et de la RAMB à l’aide des scroll-chunks on obtient deux routines ayant la structure suivante:

Routine scrollA :

_vscroll.buffer.chunk MACRO
        ldd   #0
        ldx   #0
        ldy   #0
        ldu   #0
        pshs  d,x,y,u
ENDM

_vscroll.buffer.line MACRO
        _vscroll.buffer.chunk
        _vscroll.buffer.chunk
        _vscroll.buffer.chunk
        _vscroll.buffer.chunk
        _vscroll.buffer.chunk
ENDM

_vscroll.buffer.linex8 MACRO
        _vscroll.buffer.line
        _vscroll.buffer.line
        _vscroll.buffer.line
        _vscroll.buffer.line
        _vscroll.buffer.line
        _vscroll.buffer.line
        _vscroll.buffer.line
        _vscroll.buffer.line
ENDM

scrollA     
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        jmp   returnFromScrollA

Chacune des deux routines occupe 15 * 5 * 8 * 25 octets soit 15000 octets pour les scroll-chunks. Il faudra donc consacrer quasiment deux pages de RAM à la routine d’affichage du scroll.

Dans cet exemple les données sont initialisées à 0. Il faudra bien entendu valoriser la valeur de chaque pixel dans les LDx durant la phase d’exécution du programme. On peut aussi initialiser les données à l’aide d’outils de conversion d’une image en code.

A cette étape il n’y a toujours pas de scroll, le code ne fait qu’écrire inlassablement les mêmes données en RAM vidéo à chaque tour de la boucle principale du programme.

Je ne détaille pas ici les mécanismes de double buffering pour ne pas rendre l’article trop complexe. Cependant le double buffering est nécessaire pour obtenir un affichage fluide et sans tearing.

A ce stade des explications, l’usage de la RAM est donc le suivant :

  • 2 pages de 16Ko de RAM vidéo A/B sont utilisées pour le double buffering
  • 2 pages de 16Ko de RAM sont utilisées pour les routines de scroll

Etape 3 : Self-modifying code

L’assembleur nous permet des libertés de programmation comme l’auto modification de code. cela consiste à remplacer des instructions ou des données lors de l’exécution du programme.

Pour faire varier l’affichage et réaliser un scroll vertical on peut appeller la routine contenant les scroll-chunks en des points d’entrée différents à chaque tour de boucle principale. En avançant ou en reculant l’appel de 75 octets on fera varier l’écriture des données en mémoire vidéo d’une ligne. En utilisant cette méthode le scrolling ira à la vitesse de 1000000/52000 = 19,23 fps (si on fait abstraction du code nécessaire pour le double buffering et les quelques cycles nécessaire à la boucle principale et a la variabilisation de l’appel).

Si variabiliser le point d’entrée de la routine reste relativement simple, il faut gérer deux autres aspects pour que cela fonctionne :

  • La routine d’affichage doit boucler sur elle-même
  • Il faut modifier dynamiquement le code pour gérer le retour une fois les 200 lignes de pixels écrites.

Le point de sortie est appliqué dynamiquement dans les 2 routines par modification d’OPCODE, juste après l’un des PSHS qui termine la ligne de scroll. Il s’agit d’écrire une instruction JMP qui permet le retour vers l’algorithme qui pilote le scroll, cette adresse retour étant fixe et connue. Avant de modifier le code on effectue donc une sauvegarde du code écrasé pour pouvoir le repositionner par la suite.

 Opcode/post-bytes        ASM Code       
 -----------------        -------------  

                          @loop

 ...                                                  ; some scroll-chunks

 CC 00 00                 ldd   #$0000                ; last scroll-chunck
 8E 00 00                 ldx   #$0000    
 10 8E 00 00              ldy   #$0000    
 CE 00 00                 ldu   #$0000    
 34 76                    pshs  u,y,x,d   

(CC 00 00) -> 7E 77 77   (ldd   #$0000) -> jmp #$7777 ; return to caller without using S
 8E 00 00                 ldx   #$0000                ; unusable scroll-chunck
 10 8E 00 00              ldy   #$0000                ; ...
 CE 00 00                 ldu   #$0000                ; ...
 34 76                    pshs  u,y,x,d               ; ...

 ...                                                  ; 4x unusable scroll-chunck (whole pixel line)

 CC 00 00                 ldd   #$0000                ; first scroll-chunck (routine entry)
 8E 00 00                 ldx   #$0000    
 10 8E 00 00              ldy   #$0000    
 CE 00 00                 ldu   #$0000    
 34 76                    pshs  u,y,x,d  

...                                                    ; some scroll-chunck

 7E 00 00                 jmp   @loop

Ce mécanisme de gestion du retour de la routine vient “obturer” un scroll-chunk de manière temporaire, ce qui rend inopérant l’utilisation de la ligne entière. En conséquence un ensemble de 5 scroll-chunks supplémentaires (une ligne de pixels) doit être ajouté aux deux routines.

Routine de scroll finale :

@loop   _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.linex8
        _vscroll.buffer.line
        jmp   @loop

La mise en oeuvre de ces routines de scroll est pilotée par le code suivant :

; -----------------------------------------------------------------------------
; vscroll.do
; -----------------------------------------------------------------------------
; input  REG : none
; -----------------------------------------------------------------------------
; S register is used to write in video buffer, you should expect irq calls
; to write 12 bytes into or just before video memory.
; If it occurs at the end of the buffer routine, before S is retored,
; it will erase bytes at $9FF4-$9FFF and $BFF4-$BFFF, so leave this aera unsed.
; -----------------------------------------------------------------------------
vscroll.do
        lda   vscroll.obj.bufferB.page
        ldx   vscroll.obj.bufferB.address
@loop   _SetCartPageA                  ; mount page that contain buffer code
        ldb   vscroll.cursor           ; screen start line (0-199)
        addb  vscroll.viewport.height  ; viewport size (1-200)
        bcs   @cycle
        cmpb  #vscroll.BUFFER_LINES
        bls   >
@cycle  subb  #vscroll.BUFFER_LINES    ; cycling in buffer
!       lda   #vscroll.LINE_SIZE
        mul
        leau  d,x                      ; set u where a jump should be placed for return to caller
        pulu  a,y                      ; save 3 bytes in buffer that will be erased by the jmp return
        stu   @save_u
        pshs  a,y
        lda   #m6809.OPCODE_JMP_E      ; build jmp instruction
        ldy   #@ret                    ; this works even at the end of table because there is 
        sta   -3,u                     ; already a jmp for looping into the buffer
        sty   -2,u                     ; no need to have some padding
        sts   @save_s
        lds   #$BF40
vscroll.viewport.ram equ *-2
        lda   vscroll.cursor
        ldb   #vscroll.LINE_SIZE
        mul
        leax  d,x                      ; set starting position in buffer code
        jmp   ,x
@ret    lds   #0
@save_s equ   *-2
        ldu   #0
@save_u equ   *-2
        puls  a,x
        pshu  a,x                      ; restore 3 bytes in buffer
        lda   vscroll.viewport.ram
        cmpa  #$C0
        bhs   >                        ; exit if second buffer code as been executed
        adda  #$20                     ; else execute second buffer code
        sta   vscroll.viewport.ram
        lda   vscroll.obj.bufferA.page
        ldx   vscroll.obj.bufferA.address
        bra   @loop
!       lda   vscroll.viewport.ram
        suba  #$20
        sta   vscroll.viewport.ram     ; restore to first buffer
        rts

Etape 4 : Tiles and Tilemap

Maintenant que le scroll est opérationnel il suffit d’implémenter un système de tilemap classique pour mettre à jour les données des scroll-chunks.

Un des intérêts de l’algorithme de scroll présenté ici est que les données des pixels sont persistées directement dans le code des scroll-chunks. Lorsque le viewport se déplace, on a seulement besoin de mettre à jour les pixels apparaissant à l’écran, ceux déjà présents dans les scroll-chunks ne bougent pas.

La mise à jour des données de pixel est effectuée par automodification de code dans la routine vscroll.copyBitmap du code ci dessous :

; update gfx in buffer code
; -------------------------
vscroll.updategfx
        jsr   vscroll.computeBufferWAddress
        tst   <vscroll.loop.counter
        lbeq  @exit                          ; when viewport shrink nothing to render
        ldx   vscroll.obj.bufferA.address
        leax  d,x
        stx   <vscroll.buffer.wAddressA
        ldx   vscroll.obj.bufferB.address
        leax  d,x
        stx   <vscroll.buffer.wAddressB
        ; compute current line in tile
        ldb   map.CF74021.DATA
        stb   <vscroll.backBuffer            ; backup back video buffer
        lda   vscroll.camera.lastY+1         ; LSB only
        adda  <vscroll.skippedLines          ; nb skip lines (outside viewport)
        ldb   vscroll.speed
        bpl   >
        deca                                 ; next line in tile
        ldb   #$4A ; deca
        ldu   #0
        ldx   #0
        ldy   #-1
        bra   @mod
!       adda  vscroll.viewport.height
        inca                                 ; previous line in tile
        ldb   #$4C ; inca
        ldu   vscroll.viewport.height.w
        ldx   #-vscroll.LINE_SIZE*2
        ldy   #1
@mod
        anda  #$0f                           ; modulo to keep 0-15      
        sta   <vscroll.tileset.line
        ; setup dynamic code in main scroll loop
        sty   @direction
        stb   @direction2
        stu   @direction3
        stx   @direction4
        stx   @direction5
        ldd   vscroll.camera.lastY
        addd  #0                             ; add viewport when going down
@direction3 equ *-2
@loop   
        addd  #0
@direction equ *-2
        cmpd  vscroll.map.height
        bge   >
        tsta
        bpl   @end1
        addd  vscroll.map.height
        bra   @end1
!       subd  vscroll.map.height
@end1   std   <vscroll.camera.currentY
;
        jsr   vscroll.updateTileCache        ; check cache for this line number (in d)
        lda   vscroll.obj.bufferA.page
        _SetCartPageA                        ; mount in cartridge space
        lda   <vscroll.tileset.line
        lsla
        ldx   #vscroll.obj.tile.adresses     ; load A tileset addr
        ldy   a,x
        ldx   #vscroll.obj.tile.pages        ; load A tileset page
        lda   a,x
        sta   map.CF74021.DATA               ; mount in data space
        ldu   <vscroll.buffer.wAddressA
        jsr   vscroll.copyBitmap             ; copy bitmap for buffer A
        leau  -vscroll.LINE_SIZE*2,u
@direction4 equ *-2
        cmpu  vscroll.obj.bufferA.address
        bge   @tendA
        leau  vscroll.BUFFER_LINES*vscroll.LINE_SIZE,u
        bra   >
@tendA  cmpu  vscroll.obj.bufferA.end
        blt   >
        leau  -vscroll.BUFFER_LINES*vscroll.LINE_SIZE,u
!       stu   <vscroll.buffer.wAddressA
;
        lda   vscroll.obj.bufferB.page
        _SetCartPageA                        ; mount in cartridge space
        lda   <vscroll.tileset.line
        lsla
        ldx   #vscroll.obj.tile.adresses     ; load B tileset addr
        ldy   a,x
        inca                                 ; load B tileset page
        ldx   #vscroll.obj.tile.pages
        lda   a,x
        sta   map.CF74021.DATA               ; mount in data space
        ldu   <vscroll.buffer.wAddressB
        jsr   vscroll.copyBitmap             ; copy bitmap for buffer B
        leau  -vscroll.LINE_SIZE*2,u
@direction5 equ *-2
        cmpu  vscroll.obj.bufferB.address
        bge   @tendB
        leau  vscroll.BUFFER_LINES*vscroll.LINE_SIZE,u
        bra   >
@tendB  cmpu  vscroll.obj.bufferB.end
        blt   >
        leau  -vscroll.BUFFER_LINES*vscroll.LINE_SIZE,u
!       stu   <vscroll.buffer.wAddressB
;
        lda   <vscroll.tileset.line
        inca
@direction2 equ *-1
        anda  #$0f
        sta   <vscroll.tileset.line
;
        ldd   <vscroll.camera.currentY
        dec   <vscroll.loop.counter
        lbne  @loop
@exit
        ldb   vscroll.speed
        bpl   >
        ldb   #$ff
        bra   @end2
!       clrb
@end2   stb   vscroll.speed
        ldb   <vscroll.backBuffer            ; restore back video buffer
        stb   map.CF74021.DATA
        rts

; update the horizontal line of tile id in map cache
; --------------------------------------------------
vscroll.updateTileCache
        andb  #$f0                     ; tile height is 16px, faster check here than _asrd*4
        cmpd  vscroll.map.cache.y
        bne   >
        rts                            ; return, cache is already up to date
!       std   vscroll.map.cache.y      ; load cache at a new position
        lda   vscroll.obj.map.page
        _SetCartPageA                  ; mount page that contain map data
        ldx   vscroll.obj.map.address
        lda   vscroll.map.cache.y      ; handle up to 512 lines in map, b already loaded
        _lsrd                          ; divide
        _lsrd                          ; by
        _lsrd                          ; 16 to get
        _lsrd                          ; line number in map
        _lsrd                          ; divide line in map by two
        bcc   >                        ; branch if line in map is even
        leax  30,x                     ; if line in map is odd, offset position in 
                                       ; map by 30 bytes (12bits id * 20 tiles)
!       lda   #60                      ; 2 lines of 30 bytes (12bits id * 20 tiles)
        mul                            ; mult by line/2
        leax  d,x                      ; x point to desired data map line
        ldy   #vscroll.map.cache
        lda   #20/2                    ; nb byte to load/2
        sta   <vscroll.loop.counter2
@loop   ldd   ,x+                      ; load cache by unpacking tile id
        _lsrd                          ; from 12bit to 16bit
        _lsrd
        _lsrd
        _lsrd
        std   ,y++
        ldd   ,x++
        anda  #$0F
        std   ,y++
        dec   <vscroll.loop.counter2
        bne   @loop
        rts

; copy the tile bitmap to the code buffer
; ---------------------------------------
vscroll.copyBitmap
        ldx   #vscroll.map.cache.end   ; read tiles in reverse order (from right to left)
@loop
        leax  -8,x                     ; move to next tile id in cache (to the left)
        ldd   6,x                      ; load tile id
        ldd   d,y                      ; load 4 pixels of this tile line
        std   11,u                     ; fill the LDU
        ldd   4,x                      ; load tile id
        ldd   d,y                      ; load 4 pixels of this tile line
        std   8,u                      ; fill the LDY
        ldd   2,x                      ; load tile id
        ldd   d,y                      ; load 4 pixels of this tile line
        std   4,u                      ; fill the LDX
        ldd   ,x                       ; load tile id
        ldd   d,y                      ; load 4 pixels of this tile line
        std   1,u                      ; fill the LDD
        leau  15,u                     ; move to next dest block in code buffer
        cmpx  #vscroll.map.cache
        bne   @loop
        rts

; compute write location in buffer
; --------------------------------
vscroll.computeBufferWAddress

        ; compute number of lines to render
        ldd   #0
        std   <vscroll.skippedLines        ; init tmp value
        ldb   vscroll.speed
        bpl   >
        comb                               ; by truncating, negative is floor and positive is ceil, 
                                           ; so make it ceil also for negative
!       cmpb  vscroll.viewport.height      ; compare to viewport height
        bls   >
        subb  vscroll.viewport.height
        stb   <vscroll.skippedLines+1      ; number of skipped lines (outside of viewport)
        ldb   vscroll.viewport.height      ; keep lowest value
!       stb   <vscroll.loop.counter        ; setup nb of line to render

        ; compute relative write location in code buffer
        tst   vscroll.speed
        bmi   @goUp
@goDown
        addd  vscroll.cursor.w
        subd  #1
        subd  <vscroll.skippedLines        ; skip lines if needed
        bmi   @loop
        cmpd  #vscroll.BUFFER_LINES
        bhs   @loop2
        bra   >
@loop
        addd  #vscroll.BUFFER_LINES    ; cycling in buffer
        bmi   @loop
        bra   >
@goUp
        negb   ; substract it to cursor + viewport height
        sex    ; omg !
        addd  vscroll.cursor.w
        addd  vscroll.viewport.height.w
        addd  <vscroll.skippedLines
        cmpd  #vscroll.BUFFER_LINES
        blo   >
@loop2
        subd  #vscroll.BUFFER_LINES    ; cycling in buffer
        cmpd  #vscroll.BUFFER_LINES
        bhs   @loop2
!       lda   #vscroll.LINE_SIZE
        mul
        rts

Etape 5 : Frame Drop conpensation

Pour obtenir un défilement stable, au travers d’une IRQ à 50Hz, on compte le nombre de frame drop avéré et on compense ainsi le nombre de lignes à scroller.

vscroll.move

; update position in map and buffer
; ---------------------------------

        ; check for elapsed frames
        lda   gfxlock.frameDrop.count
        bne   >
@exit   rts
;
        ; compute frame compensated speed
!       sta   <vscroll.loop.counter
        ldd   vscroll.speed                  ; load speed value of previous frame
!       addd  vscroll.camera.speed           ; mult speed by frame drop
        dec   <vscroll.loop.counter
        bne   <
;
        ; exit if speed is too small (subpixel)
        stb   vscroll.speed+1
        sta   vscroll.speed
        adda  #128 ; this cryptic code negate integer part of a 8.8 value
        eora  #127 ; and round by floor
        sbca  #255 ; cursor goes the opposite direction of y in buffer
        beq   @exit

        ; compute cursor in cycling buffer code (modulo)
        tfr   a,b
        sex
        bpl   @goUp
@goDown
        addd  vscroll.cursor.w
        bpl   @end
!       addd  #vscroll.BUFFER_LINES
        bmi   <
        bra   @end
@goUp
        addd  vscroll.cursor.w
        cmpd  #vscroll.BUFFER_LINES
        blo   @end
!       subd  #vscroll.BUFFER_LINES
        cmpd  #vscroll.BUFFER_LINES
        bhs   <
@end    stb   vscroll.cursor

        ; compute position in map
        ldx   vscroll.camera.y
        stx   vscroll.camera.lastY
        ldb   vscroll.speed  ; get int part of 8.8
        bpl   >
        incb                 ; by truncating, negative is floor and positive is ceil, 
                             ; so make it ceil also for negative
!       leax  b,x            ; do not use abx, b is signed, speed is implicitly caped 
                             ; to a choppy 127px by frame

        ; wrap camera position in map (infinite level loop)
        tfr   x,d
        cmpx  vscroll.map.height
        bge   >
        tsta
        bpl   @end
        addd  vscroll.map.height
        bra   @end
!       subd  vscroll.map.height
@end    std   vscroll.camera.y

Conclusion

En terme de rendu final, à ce stade, on obtient alors ceci :

Ce scrolling a été implémenté dans notre moteur de jeux dans sa version verticale pour BattleSquadron et Goldorak. Il est tout à fait possible de faire une version multidirectionnelle … un jour peut-être !

Annexe

Quelques compléments sur la structure des données …

; -----------------------------------------------------------------------------
; Vertical Scroll
; -----------------------------------------------------------------------------
; wide-dot - Benoit Rousseau - 11/09/2023
; ---------------------------------------
; - use a cycling code buffer to render a vertical scroll
; - buffer use stack blasting (pshs d,x,y,u)
; - buffer is only updated few lines per frame (only the new lines)
; - scroll is bi-directionnal
; - speed is a fixed point value and adjusted in regard of frame drop
; - handle up to 512 lines of tiles in a map
; -----------------------------------------------------------------------------

        opt c

; constants
; -----------------------------------------------------------------------------
m6809.OPCODE_JMP_E          equ   $7E

vscroll.LINE_SIZE           equ   75
 IFNDEF  vscroll.BUFFER_LINES
vscroll.BUFFER_LINES        equ   201  ; nb lines in buffer is 201 (0-200 to fit JMP return)
 ENDC

; parameters
; -----------------------------------------------------------------------------
; scroll data is split in 5 pages
vscroll.obj.map.page        fcb   0
vscroll.obj.map.address     fdb   0
vscroll.obj.tile.pages      fill  0,32 ; pages for every line tileset A and B
vscroll.obj.tile.adresses   fill  0,32 ; starting position for every line tileset (generic)
vscroll.obj.tile.nbx2       fdb   0
vscroll.obj.bufferA.page    fcb   0
vscroll.obj.bufferA.address fdb   0
vscroll.obj.bufferA.end     fdb   0
vscroll.obj.bufferB.page    fcb   0
vscroll.obj.bufferB.address fdb   0
vscroll.obj.bufferB.end     fdb   0
vscroll.camera.speed        fdb   0    ; (signed 8.8 fixed point) nb of pixels/50hz

; private variables
; -----------------------------------------------------------------------------
vscroll.cursor.w            fcb   0    ; padding for 16 bit operations
vscroll.cursor              fcb   0
vscroll.speed               fdb   0    ; (signed 8.8 fixed point) nb of line to scroll
vscroll.map.height          fdb   0    ; map height in pixels
vscroll.map.cache.y         fdb   -1   ; current cached map line
vscroll.map.cache           fill  0,40 ; a full unpacked map line with 20x tile ids
vscroll.map.cache.end       equ   *
vscroll.viewport.height.w   fcb   0    ; padding for 16 bit operations
vscroll.viewport.height     fcb   0
vscroll.viewport.y          fcb   0    ; y position of viewport on screen
vscroll.camera.y            fdb   0    ; camera position in map
vscroll.camera.lastY        fdb   0    ; last camera position in map

; -----------------------------------------------------------------------------
; vscroll.move
; -----------------------------------------------------------------------------
; input  REG : none
; -----------------------------------------------------------------------------

; temporary variables in dp
vscroll.loop.counter        equ dp_extreg    ; BYTE
vscroll.loop.counter2       equ dp_extreg+1  ; BYTE
vscroll.backBuffer          equ dp_extreg+2  ; BYTE
vscroll.buffer.wAddressA    equ dp_extreg+3  ; WORD
vscroll.buffer.wAddressB    equ dp_extreg+5  ; WORD
vscroll.camera.currentY     equ dp_extreg+7  ; WORD
vscroll.skippedLines        equ dp_extreg+9  ; WORD
vscroll.tileset.line        equ dp_extreg+11 ; BYTE