[SNES] Créer une VWF

Foire aux questions et tutoriels
neige
Nouveau Floodeur
Messages : 22
Inscription : 19 oct. 2008, 02:39

[SNES] Créer une VWF

Message non lupar neige » 11 déc. 2011, 02:14

Suite à une demande sur les VWF et aussi parce que je n'ai plus le temps de participer activement à des projets, j'ai décidé d'expliquer comment transformer la routine d'affichage de ce jeu en VWF. Du à un manque d'espace à l'endroit où la routine est placée dans la ROM, j'ai du faire des modifications qui n'ont pas directement rapport avec la VWF mais j'espère que les concepts de base seront quand même clairs.

La première chose à faire est de trouver de l'espace dans la RAM pour quelques variables nécessaires.

Il faut 1 octet pour la variable qui contient le nombre de pixels de large déjà occupé dans la colonne courante (que j'ai appelée Vwf.BitsUsed), cette variable est la plus importante et ne doit pas être modifié par le reste du programme tant qu'on n'a pas fini d'afficher le texte.

Il faut 1 octet pour la variable qui contient l'offset dans la table de sauts ou le compteur de boucles (Vwf.ShiftOffset), tout dépendant de la méthode utilisée, de la routine qui fait glisser les lignes de pixels. Cette variable est utilisé que jusqu'à la boucle de copie, elle n'est pas essentielle et on peux s'en passer si on modifie un peu la routine de translation en échange de plus d'espace utilisé dans la ROM et d'un ralentissement.

Il faut 1 octet pour la variable qui contient les pixels qui débordent lors de la translation (Vwf.Spill). Cette variable est essentielle mais n'est utilisée que dans la boucle de copie.

Il faut au moins 6 octets pour les entrées de tilemaps à envoyer à la VRAM (Dlg.TilemapEntry1 à Dlg.TilemapEntry3), 12 octets si on construit toutes les entrées à la fois (Dlg.TilemapEntry1 à Dlg.TilemapEntry6).

Pour les variables qui contiennent le masque de bits qui sert à éliminer les pixels indésirables (Stack.Bitmask) et le nombre de colonnes à mettre à jour (Stack.NumCols) j'ai utilisé la pile (stack en anglais).

Après investigation, ce jeu a beaucoup d'espace libre en RAM, donc j'ai programmé la routine d'affichage en utilisant cet espace, mais je vais quand même expliquer ici comment on aurait pu faire si ce n'avait pas été le cas.

Si on étudie la routine originale on peut remarquer que les adresses de $0000 à $0006 sont utilisées sans les lire donc on peut assumer qu'elles ne contiennent rien d'important. $0000 est utilisé pour le compteur de boucles donc on ne peut pas l'utiliser, $0002 est utilisé pour calculer l'adresse dans la fenêtre de dialogue donc on ne peut pas l'utiliser pour Vwf.ShiftOffset mais puisqu'elle n'est pas utilisée par la suite on peut y mettre Vwf.Spill ce qui laisse $0004 pour Vwf.ShiftOffset. Et si on construit les entrées de tilemaps de la ligne du haut en utilisant Dlg.TilemapEntry1 à Dlg.TilemapEntry3 et qu'on les envoie tout de suite, on peut réutiliser ces variables pour les entrées de la ligne du bas et ainsi libérer l'adresse de Dlg.TilemapEntry4 pour y mettre Vwf.BitsUsed.

Il faut aussi trouver de l'espace dans la ROM pour la table de largeur de caractères, 1 octet par caractère (Dlg.FontWidth).

La première partie du code ne fait pas partie du programme en tant que tel mais d'une technique que j'utilise pour réduire les risque d'erreurs lors de l'utilisation de variables sur la pile. En associant un nom à un emplacement dans la pile, on n'a qu'un endroit à modifier lorsqu'on ajoute ou enlève une variable de la pile.

Code : Tout sélectionner

Dlg.PrintChar_S:
; stack:
.DEFINE Stack.NumCols     1 ; $01,s (word)
.DEFINE Stack.Bitmask     3 ; $03,s (word)

Suit l'initialisation qu'on n'a pas besoin de changer.

Code : Tout sélectionner

                SEP     #$20 ; m
                PHB
                LDA.B   #$7E
                PHA
                PLB
                REP     #$20 ; m

Puisque qu'à ce moment dans la routine les registre X et Y sont libres, on en profite pour initialiser toutes nos variables pour la VWF, en premier l'offset dans la table de sauts de la routine de translation. Puisque qu'il faut faire glisser chaque ligne de pixels vers la droite du nombre de pixels déjà utilisé dans la colonne courante et que chaque adresse dans la table fait 2 octets, on charge le nombre de pixels déjà utilisé et on le multiplie par 2 avant de le stocker pour plus tard dans Vwf.ShiftOffset.

Code : Tout sélectionner

                LDA.W   Vwf.BitsUsed
                ASL     A
                STA.W   Vwf.ShiftOffset

On profite du fait que l'offset dans la table de masques de bits est le même que dans la table de sauts de la routine de translation pour initialiser la variable Stack.Bitmask sans avoir à recalculer l'offset, on place le résultat sur la pile.

Code : Tout sélectionner

                TAX
                LDA.L   Vwf.BitMasks, X
                PHA               ; Stack.Bitmask

La table de masques de bits est situé en dehors de la routine. Elle sert à éliminer les pixels indésirables, si il n'y a aucun pixels d'utilisé, on les efface tous, s'il y en a un d'utilisé, on conserve le premier pixels de chaque plan de bits, etc. Elle est définie comme suit:

Code : Tout sélectionner

Vwf.BitMasks:
.DW $0000, $8080, $C0C0, $E0E0, $F0F0, $F8F8, $FCFC, $FEFE

La prochaine séquence d'instruction sert à initialiser Stack.NumCols et à mettre à jour Vwf.BitsUsed. On commence par lire la largeur du caractère en utilisant son numéro en tant qu'index.

Code : Tout sélectionner

                LDA.W   Dlg.CharToPrint
                AND.W   #$00FF
                TAX
                LDA.W   Dlg.FontWidth, X
                AND.W   #$00FF

On y ajoute le nombre de pixels déjà utilisés.

Code : Tout sélectionner

                CLC
                ADC.W   Vwf.BitsUsed

Le registre X va nous servir à compter le nombre de colonnes à mettre à jour, on l'initialise à 1 car dans tout les cas on doit mettre au moins une colonne à jour même si est incomplète.

Code : Tout sélectionner

                LDX.W   #$0001

On soustrait la largeur d'une colonne, si on se retrouve avec un nombre négatif, nous n'avons qu'une colonne à mettre à jour, sinon on soustrait encore une colonne, si on se retrouve avec un nombre négatif, nous avons 2 colonnes à mettre à jour, dans tout les autres cas, nous avons 3 colonnes à mettre à jour, on soustrait quand même une colonne, la raison va être claire bientôt. On place le contenu final du registre X sur la pile.

Code : Tout sélectionner

                SEC
                SBC.W   #$0008
                BCC     _update_1_col
                SBC.W   #$0008
                BCC     _update_2_col
                SBC.W   #$0008
_update_3_col:
                INX
_update_2_col:
                INX
_update_1_col:
                PHX               ; Stack.NumCols

On rajoute la dernière colonne qu'on a soustrait pour se retrouver un nombre positif qui correspond au nombre de pixels utilisés dans la colonne après l'affichage du caractère courant, on met à jour Vwf.BitsUsed car on en aura plus besoin pour afficher le caractère courant.

Code : Tout sélectionner

                CLC
                ADC.W   #$0008
                STA.W   Vwf.BitsUsed

Les caractères ont 16 pixels de large au maximum donc on ne change pas le calcul de l'adresse du caractère dans la fonte.

Code : Tout sélectionner

                LDA.W   Dlg.CharToPrint
                AND.W   #$0007
                ASL     A
                STA.W   $0000
                LDA.W   Dlg.CharToPrint
                AND.W   #$00F8
                ASL     A
                ASL     A
                CLC
                ADC.W   $0000
                ASL     A
                ASL     A
                ASL     A
                ASL     A
                TAX

Comme on utilise les colonnes de 1 tile de large pour indiquer l'avancement sur la ligne, on utilise le même calcul de l'adresse dans la fenêtre de dialogue que pour la version 8x16.

Code : Tout sélectionner

                LDA.W   Dlg.VWFCol
                AND.W   #$00FF
                STA.W   $0000
                LDA     Dlg.VWFRow
                AND.W   #$00FF
                ASL     A
                ASL     A
                STA.W   $0002
                ASL     A
                ASL     A
                ASL     A
                ASL     A
                SEC
                SBC.W   $0002
                CLC
                ADC.W   $0000
                ASL     A
                ASL     A
                ASL     A
                ASL     A
                STA.W   Dlg.WndBufOfs
                TAY

Avant de passer à la boucle de copie, Je vais expliquer la routine de translation qui y est utilisée. Quand on appelle la routine le registre A contient les pixels de la ligne à faire glisser vers la droite.

On commence par sauvegarder le registre X qui est l'index source dans la fonte, on utilise la variable Vwf.ShiftOffset pour sauter à l'endroit dans la séquence de shifts qui correspond au nombre de pixels déjà utilisés de manière à ce la ligne courante soit placé après celle du caractère précédent.

À chaque étape de shift on fait glisser la ligne d'un pixel vers la droite et on fait glisser le pixel qui a débordé dans Vwf.Spill.

Pour finir, on inverse les octets du registre A pour que l'octet moins significatif contienne les pixels de gauche et l'autre les pixels de droite. On restaure le registre X avant de retourner à la routine appelante.

Code : Tout sélectionner

Vwf.ShiftLine_S:
                REP     #$20 ; m
                PHX
                LDX.W   Vwf.ShiftOffset
                JMP     (_jumptable, X)
_jumptable:
.DW _0, _1, _2, _3, _4, _5, _6, _7
_7:
                LSR     A
                ROR.W   Vwf.Spill
_6:
                LSR     A
                ROR.W   Vwf.Spill
_5:
                LSR     A
                ROR.W   Vwf.Spill
_4:
                LSR     A
                ROR.W   Vwf.Spill
_3:
                LSR     A
                ROR.W   Vwf.Spill
_2:
                LSR     A
                ROR.W   Vwf.Spill
_1:
                LSR     A
                ROR.W   Vwf.Spill
_0:
                XBA
                PLX
                SEP     #$20 ; m
                RTS

Par manque de place, j'ai du modifier la boucle de copie pour traiter les 2 lignes de tiles du caractère une après l'autre en ajustant les index après la première ligne. Cela ne fait pas partie de la VWF, si on aurait eu la place on aurait pu traiter les 2 lignes en même temps comme avant.

On initialise le compteur de boucle à 16 maintenant qu'on traite les lignes une après l'autre.

Code : Tout sélectionner

                LDA.W   #$0010          ; nombre de ligne de pixels dans un caractère
                STA.W   $0000

La première chose qu'on fait à chaque étape de la boucle est d'effacer les pixels qui ont débordés de la ligne précédente.

Code : Tout sélectionner

_vwf_loop:
                REP     #$20 ; m
                STZ.W   Vwf.Spill

Ensuite on efface les pixels indésirables de la première colonne.

Code : Tout sélectionner

                LDA.W   DlgWndGfxBuf, Y
                AND     Stack.Bitmask, S
                STA.W   DlgWndGfxBuf, Y

Puisqu'on doit traiter une ligne de pixels complète à chaque étape de la boucle, on ne peut plus traiter la colonne de gauche et celle de droite séparément. De plus, puisqu'on utilise des tiles au format GameBoy, on ne peut placer les lignes des 2 colonnes en même temps dans le registre A parce que 8 pixels font 16 bits et on doit traiter 16 pixels à la fois (le registre A a 16 bits). Ce qu'on doit faire c'est de traiter chaque plan de bits de la ligne séparément.

On lit le premier plan de bit de la colonne gauche, on inverse les octets du registre A pour que les pixels qu'on vient de lire se retrouve dans l'octet le plus significatif, ensuite on lit le premier plan de bit de la colonne droite. Les pixels dans le registre ressemble à ce qu'on pourrait voir, c'est à dire les pixels de gauche à gauche et ceux de droite à droite.

Code : Tout sélectionner

                SEP     #$20 ; m
                LDA.W   DlgFontGfx, X
                XBA
                LDA.W   DlgFontGfx+$10, X

On appelle ensuite la routine de translation pour faire glisser la ligne vers la droite.

Code : Tout sélectionner

                JSR     Vwf.ShiftLine_S

On combine les pixels de gauche avec ceux du caractère précédent.

Code : Tout sélectionner

                ORA.W   DlgWndGfxBuf, Y
                STA.W   DlgWndGfxBuf, Y

on inverse pour obtenir les pixels de droite qu'on met à leur place.

Code : Tout sélectionner

                XBA
                STA.W   DlgWndGfxBuf+$10, Y

On lit les pixels qui ont débordés qu'on place à la suite des autres.

Code : Tout sélectionner

                LDA.W   Vwf.Spill+1
                STA.W   DlgWndGfxBuf+$20, Y

On fait la même chose pour le deuxième plan de bits.

Code : Tout sélectionner

                LDA.W   DlgFontGfx+1, X
                XBA
                LDA.W   DlgFontGfx+$11, X
                JSR     Vwf.ShiftLine_S
                ORA.W   DlgWndGfxBuf+1, Y
                STA.W   DlgWndGfxBuf+1, Y
                XBA
                STA.W   DlgWndGfxBuf+$11, Y
                LDA.W   Vwf.Spill+1
                STA.W   DlgWndGfxBuf+$21, Y

Si on vient de terminer de traiter la huitième ligne de pixels, on doit ajuster les index. La deuxième ligne de tiles est $100 octets plus loin que la première dans la font et $1E0 plus loin dans la fenêtre de dialogue et comme les index ont déjà avancés de 16 octets nous devons soustraire ce nombre pour obtenir le nombre à ajouter.

Code : Tout sélectionner

                LDA.W   $0000
                CMP.B   #$08
                BNE     _inc_indices

_adjust_indices:
                REP     #$20 ; m
                TXA
                CLC
                ADC.W   #$00F0
                TAX
                TYA
                CLC
                ADC.W   #$01D0
                TAY
                DEC.W   $0000
                BRA     _vwf_loop

Si ce n'était pas la huitième ligne, on augmente les index comme d'habitude.

Code : Tout sélectionner

_inc_indices:
                INX
                INX
                INY
                INY
                DEC.W   $0000
                BNE     _vwf_loop

La boucle de copie est maintenant terminée et notre caractère a été copié dans la mémoire tampon de la fenêtre de dialogue. On doit l'envoyer à la VRAM. Cette partie est pratiquement identique à l'originale excepté qu'on utilise la variable Stack.NumCols pour calculer le nombre d'octets à envoyer.

Code : Tout sélectionner

_send_to_vram:
                REP     #$20 ; m
                LDA.W   Dlg.WndBufOfs
                LSR     A
                ADC.W   #$5008    ; VRAM:$A010
                STA.W   DMA.QueueItemToAdd.VRAMAddr
                LDA.W   Dlg.WndBufOfs
                CLC
                ADC.W   #DlgWndGfxBuf
                STA.W   DMA.QueueItemToAdd.SrcAddr

                LDA     Stack.NumCols, S
                ASL     A
                ASL     A
                ASL     A
                ASL     A

                STA.W   DMA.QueueItemToAdd.Size
                SEP     #$20 ; m
                LDA.B   #$80
                STA.W   DMA.QueueItemToAdd.VMAIN
                LDA.B   #$7E
                STA.W   DMA.QueueItemToAdd.SrcAddr+2
                LDA.B   #$01
                STA.W   DMA.QueueItemToAdd.DMAP
                LDA.B   #$18
                STA.W   DMA.QueueItemToAdd.DestReg
                JSL.L   DMA.QueueDMATransfer_L
                REP     #$20 ; m
                LDA.W   Dlg.WndBufOfs
                LSR     A
                CLC
                ADC.W   #$50F8    ; VRAM:$A1F0
                STA.W   DMA.QueueItemToAdd.VRAMAddr
                LDA     Dlg.WndBufOfs
                CLC
                ADC.W   #DlgWndGfxBuf+$1E0
                STA.W   DMA.QueueItemToAdd.SrcAddr
                JSL.L   DMA.QueueDMATransfer_L

On calcule maintenant les valeurs qui vont servir à créer les entrées de tilemap, cette partie est pareille à l'originale excepté qu'on utilise la variable Stack.NumCols pour le nombre d'entrées à envoyer.

Code : Tout sélectionner

_update_tilemap:
                LDA.W   Dlg.VWFCol
                AND.W   #$00FF
                STA.W   $0000
                LDA.W   Dlg.VWFRow
                AND.W   #$00FF
                ASL     A
                ASL     A
                ASL     A
                ASL     A
                ASL     A
                ASL     A
                CLC
                ADC.W   $0000
                ADC.W   #$5C41    ; VRAM:$B882
                STA.W   $0003     ; VRAMAddr
                LDA.W   #Dlg.TilemapEntry1
                STA.W   $0000     ; srcAddr
                SEP     #$20 ; m
                LDA.B   #$00
                STA.W   $0002     ; srcBank
                STZ.W   $0005     ; VMAIN

                LDA     Stack.NumCols, S
                STA.W   $0006     ; numWords

                REP     #$20 ; m
                LDA.W   Dlg.WndBufOfs
                LSR     A
                LSR     A
                LSR     A
                LSR     A
                INC     A
                STA.W   Dlg.TilemapCharNum
                LDA.W   Dlg.TextPaletteIndex
                AND.W   #$00FF
                XBA
                ASL     A
                ASL     A
                ORA.W   #$2000
                STA.W   Dlg.TilemapPalNum

On construit 3 entrées de tilemap pour chaque ligne de tiles même si elles ne sont peut-être pas toutes envoyées, c'est plus facile ainsi.

Code : Tout sélectionner

                ORA.W   Dlg.TilemapCharNum
                STA.W   Dlg.TilemapEntry1

                LDA.W   Dlg.TilemapCharNum
                CLC
                ADC.W   #$0001
                ORA.W   Dlg.TilemapPalNum
                STA.W   Dlg.TilemapEntry2

                LDA.W   Dlg.TilemapCharNum
                CLC
                ADC.W   #$0002
                ORA.W   Dlg.TilemapPalNum
                STA.W   Dlg.TilemapEntry3

                LDA.W   Dlg.TilemapCharNum
                CLC
                ADC.W   #$001E
                ORA.W   Dlg.TilemapPalNum
                STA.W   Dlg.TilemapEntry4

                LDA.W   Dlg.TilemapCharNum
                CLC
                ADC.W   #$001F
                ORA.W   Dlg.TilemapPalNum
                STA.W   Dlg.TilemapEntry5

                LDA.W   Dlg.TilemapCharNum
                CLC
                ADC.W   #$0020
                ORA.W   Dlg.TilemapPalNum
                STA.W   Dlg.TilemapEntry6

Ensuite on les envoie à la VRAM, puisque qu'on a 3 entrées de tilemap on doit augmenter l'adresse source de 6 au lieu de 4 et comme précédemment on utilise la variable Stack.NumCols pour le nombre d'entrées à envoyer.

Code : Tout sélectionner

                JSL.L   VRAM.QueueTransfer_L
                LDA.W   $0003
                CLC
                ADC.W   #$0020
                STA.W   $0003     ; VRAMAddr
                LDA.W   $0000
                CLC
                ADC.W   #$0006    ; Maintenant 6
                STA.W   $0000     ; srcAddr
                SEP     #$20 ; m
                LDA.B   #$00
                STA.W   $0002     ; srcBank
               
                LDA     Stack.NumCols, S
                STA.W   $0006     ; numWords
               
                JSL.L   VRAM.QueueTransfer_L

Pour ajuster le numéro de la colonne dans la fenêtre de dialogue, on utilise encore la variable Stack.NumCols, mais on doit auparavant la décrémenter car la dernière colonne mise à jour n'est pas complète, c'est à dire qu'on peut y ajouter une partie du prochain caractère.

Code : Tout sélectionner

                LDA     Stack.NumCols, S
                DEC     A
                CLC
                ADC.W   Dlg.VWFCol
                STA.W   Dlg.VWFCol

Si on doit sauter à la ligne, on réinitialise la variable Vwf.BitsUsed pour commencer au début de la tile.

Code : Tout sélectionner

                CMP.B   #030            ; max char on line
                BCC     _no_newline
                STZ.W   Vwf.BitsUsed
                STZ.W   Dlg.VWFCol
                INC     Dlg.VWFRow

Avant de quitter la routine on doit libérer l'espace qu'on a réservé sur la pile.

Code : Tout sélectionner

_no_newline:
                PLX               ; libérer Stack.NumCols
                PLX               ; libérer Stack.Bitmask
                PLB
                RTS

Il resterait quelques détails à ajuster comme réinitialiser la variable Vwf.BitsUsed dans la routine d'initialisation de la fenêtre de dialogue et dans les codes de saut de ligne et de déplacement du curseur ainsi que de vérifier que les menus sont bien alignés mais cela dépasse le cadre de cette explication.

neige
Nouveau Floodeur
Messages : 22
Inscription : 19 oct. 2008, 02:39

Re: [SNES] Créer une VWF

Message non lupar neige » 20 févr. 2012, 03:07

Je recommande de lire l'analyse de la routine originale dans le premier message de ce fil de discussion avant.


Revenir vers « FAQ »

Qui est en ligne ?

Utilisateurs parcourant ce forum : Aucun utilisateur inscrit et 1 invité