Coder un sine scroll sur Amiga (3/5)

Cet article est le troisième d’une série de cinq consacrés à la programmation d’un one pixel sine scroll sur Amiga, un effet très utilisé par les coders de démos et autres cracktros durant un temps. Par exemple, dans cette cracktro du groupe Supplex :
Sine scroll dans une cracktro du groupe Supplex
Dans le premier article, nous avons vu comment installer en environnement de développement sur un Amiga émulé avec WinUAE et coder la Copper list de base pour afficher quelque chose à l’écran. Dans le deuxième article, nous avons vu comment préparer une police de caractères 16×16 pour en afficher facilement les colonnes de pixels des caractères, précalculer les valeurs du sinus requises pour déformer le texte en modifiant l’ordonnée des colonnes, et mettre en place un triple buffering pour alterner proprement les images à l’écran.
Dans ce troisième article, nous allons rentrer dans le vif du sujet en voyant comment dessiner et animer le sine scroll, d’abord au CPU, puis au Blitter.
Cliquez ici pour télécharger l’archive contenant le code et les données du programme présenté ici – c’est la même que dans les autres articles.
NB : Cet article se lit mieux en écoutant l’excellent module composé par Nuke / Anarchy pour la partie magazine de Stolen Data #7, mais c’est affaire de goût personnel…

Faire défiler et animer le sine scroll

La boucle principale peut maintenant être intégralement décrite. Elle accomplit successivement les tâches suivantes à chaque itération :
  • attendre que le faisceau d’électron a terminé de dessiner la trame ;
  • permuter circulairement les trois bitplanes pour afficher la dernière image ;
  • attendre le Blitter et démarrer l’effacement du bitplane C contenant la pénultième image ;
  • dessiner le texte dans le bitplane B qui contenait l’antépénultième image ;
  • animer l’indice de la première colonne du premier caractère du texte à dessiner ;
  • animer le sinus de cette première colonne ;
  • tester si le bouton gauche de la souris est pressé.
Les trois premières tâches ont déjà été décrites. Nous allons donc décrire les suivantes.
Le sine scroll est dessiné par une boucle qui dessine SCROLL_DX colonnes de caractères consécutifs du texte à partir de la colonne SCROLL_X dans le bitplane. Les indices de la première colonne à dessiner et du premier caractère dont elle provient sont conservés dans les variables scrollColumn et scrollChar, respectivement. L’offset du sinus de la première colonne du sine scroll est conservé dans une variable angle.
Réglons immédiatement la question de l’animation du sine scroll dans la boucle principale.
Faire défiler le texte le long d’un sinus ne présenterait pas beaucoup d’intérêt si ce dernier n’était pas lui-même animé : il donnerait simplement l’impression de parcourir des montagnes russes. Pour cela, nous décrémentons l’offset du sinus de la première colonne du sine scroll à chaque trame, non sans oublier de gérer l’éventuel débordement :
	move.w angle,d0
	sub.w #(SINE_SPEED_FRAME<<1),d0
	bge _angleFrameNoLoop
	add.w #(360<<1),d0
_angleFrameNoLoop:
	move.w d0,angle
De plus, le texte doit défiler de gauche à droite. Pour cela, nous incrémentons de SCROLL_SPEED l'indice de la première colonne dans le texte. Ici, nous devons gérer deux éventuels débordements : le débordement d'un caractère pour passer de la dernière colonne d'un caractère à la première du caractère suivant, et le débordement du texte pour passer du dernier caractère du texte au premier :
	move.w scrollColumn,d0
	addq.w #SCROLL_SPEED,d0
	cmp.b #15,d0				;La colonne est après la dernière du caractère ?
	ble _scrollNextColumn		;Si non, en rester là
	sub.b #15,d0				;Si oui, colonne dans le caractère suivant...
	move.w scrollChar,d1
	addq.w #1,d1				;...et passage au caractère suivant
	lea text,a0
	move.b (a0,d1.w),d2
	bne _scrollNextChar		;Le caractère est après le dernier caractère ?
	clr.w d1					;Si oui, reboucler sur le premier caractère
_scrollNextChar:
	move.w d1,scrollChar
_scrollNextColumn:
	move.w d0,scrollColumn
Nous pouvons maintenant passer au dessin du sine scroll. Ce dernier est assuré par la boucle évoquée plus tôt, qui est logée dans la boucle principale. Avant d'entamer cette boucle, nous devons procéder à un certain nombre d'initialisations dans l'objectif de maximiser l'utilisation des registres pour minimiser la nécessité d'aller chercher des données en mémoire.
Tout d'abord, nous déterminons l'offset (D6) du mot dans le bitplane dans lequel se trouve le bit qui correspond à la première colonne où dessiner, et nous identifions ce bit (D7) :
	;Déterminer l'offset du mot du bitplane de la première colonne où dessiner

	moveq #SCROLL_X,d6
	lsr.w #3,d6	; Offset de l'octet de la colonne dans le bitplane
	bclr #0,d6		; Offset du mot (revient à lsr.w #4 puis lsl.w #1)

	;Déterminer le bit de ce mot correspondant à cette colonne

	moveq #SCROLL_X,d4
	and.w #$000F,d4
	moveq #15,d7
	sub.b d4,d7	;Bit dans le mot
Ensuite, nous déterminons l'adresse (A0) du caractère courant ainsi que celle (A1) du mot dans la police 16x16 de sa colonne courante (D4) à dessiner dans la colonne courante du bitplane évoquée à l'instant :
	move.w scrollChar,d0
	lea text,a0
	lea (a0,d0.w),a0
	move.w scrollPixel,d4
	clr.w d1
	move.b (a0)+,d1
	subi.b #$20,d1
	lsl.w #5,d1				;32 octets par caractère dans la police 16x16
	move.w d4,d2				;Colonne du caractère à dessiner
	lsl.w #1,d2				;2 octets par ligne dans la police 16x16
	add.w d2,d1
	move.l font16,a1
	lea (a1,d1.w),a1			;Adresse de la colonne à dessiner
Dans le code précédent, l'offset de la première colonne d'un caractère se déduit du code ASCII de ce caractère auquel il faut soustraite $20 - aspect pratique de la police 8x8 qui a servi de base à la police 16x16, les caractères sont ainsi triés.
Enfin, nous procédons à des initialisations diverses de registres utilisés de manière récurrente dans la boucle, dont l'offset du sinus de la colonne courante (D0) et le nombre de colonnes restant à dessiner (D1) :
	move.w angle,d0
	move.w #SCROLL_DX-1,d1
	move.l bitplaneB,a2
In fine, les registres se présentent ainsi au démarrage de la boucle :
RegistreContenu
D0Offset du sinus de la colonne courante où dessiner dans le bitplane
D1Colonne courante où dessiner dans le bitplane
D4Colonne courante du caractère courant à dessiner
D6Offset du mot dans le bitplane qui contient la colonne courante où dessiner
D7Bit dans ce mot qui correspond à cette colonne
A0Adresse du caractère courant dans le texte
A1Adresse du mot dans la police correspondant à la colonne courante de ce caractère
A2Adresse du bitplane où dessiner
La boucle de dessin des SCROLL_DX colonnes du sine scroll accomplit successivement les tâches suivantes à chaque itération :
  • calculer l'adresse du mot contenant le premier pixel de la colonne dans le bitplane ;
  • afficher la colonne courante du caractère courant ;
  • passer à la colonne suivante du caractère courant, ou à la première colonne du caractère suivant, ou à celle du premier caractère du texte ;
  • décrémenter l'angle de la colonne courante.
Le calcul de l'adresse (A4) du mot contenant le premier pixel de la colonne dans le bitplane s'appuie sur une multiplication par une valeur de sinus précalculée par une puissance de 2, comme nous l'avons déjà vu :
	lea sinus,a6
	move.w (a6,d0.w),d1
	muls #(SCROLL_AMPLITUDE>>1),d1
	swap d1
	rol.l #2,d1
	add.w #SCROLL_Y+(SCROLL_AMPLITUDE>>1),d1
	move.w d1,d2
	lsl.w #5,d1
	lsl.w #3,d2
	add.w d2,d1	;D1=(DISPLAY_DX>>3)*D1=40*D1=(32*D1)+(8*D1)=(2^5*D1)+(2^3*D1)
	add.w d6,d1
	lea (a2,d1.w),a4
Oui ! D1 est utilisé ici comme variable temporaire alors qu'il a été initialisé pour servir de compteur à la boucle. C'est qu'ainsi que nous allons bientôt le constater, nous sommes à court de registres. En conséquence, la boucle commence et se termine par de brefs échanges avec le pile :
_writeLoop:
	move.w d1,-(sp)
	;...
	move.w (sp)+,d1
	dbf d1,_writeLoop
Nous pouvons alors afficher la colonne courante du caractère courant dans la colonne courante du bitplane. Le principe de l'affichage est assez simple du fait que la police 16x16 a subi une rotation qui permet de tester les bits successifs d'un mot correspondant à la colonne à afficher plutôt que de tester le même bit de mots successifs :
Affichage d'un caractère pixel par pixel
Afficher la colonne courante (mot en A1) du caractère courant dans la colonne courante du bitplane (bit D7 du mot en A4), cela donne :
	move.w (a1),d1
	clr.w d2
	moveq #LINE_DX,d5
_columnLoop:
	move.w (a4),d3
	btst d2,d1
	beq _pixelEmpty
	bset d7,d3
	bra _pixelFilled
_pixelEmpty:
	bclr d7,d3
_pixelFilled:
	move.w d3,(a4)
	lea DISPLAY_DX>>3(a4),a4
	addq.b #1,d2
	dbf d5,_columnLoop
Cet affichage accompli, nous pouvons passer à celui de la colonne suivante du texte, c'est-à-dire la colonne suivante du caractère courant ou la première colonne du caractère suivant, à moins que le caractère courant ne soit le dernier et que nous devions reboucler sur le premier caractère du texte, répétant le texte du sine scroll à l'infini :
	addq.b #1,d4
	btst #4,d4
	beq _writeKeepChar
	bclr #4,d4
	clr.w d1
	move.b (a0)+,d1
	bne _writeNoTextLoop
	lea text,a0
	move.b (a0)+,d1
_writeNoTextLoop
	subi.b #$20,d1
	lsl.w #5,d1
	move.l font16,a1
	lea (a1,d1.w),a1
	bra _writeKeepColumn
_writeKeepChar:
	lea 2(a1),a1
_writeKeepColumn:
Cette colonne suivante se trouvera à une autre ordonnée, déterminée en décrémentant l'offset courant du sinus... :
	subq.w #(SINE_SPEED_PIXEL<<1),d0
	bge _anglePixelNoLoop
	add.w #(360<<1),d0
_anglePixelNoLoop:
...et elle sera affichée dans la colonne suivante du bitplane, éventuellement dans le mot suivant le mot courant :
	subq.b #1,d7
	bge _pixelKeepWord
	addq.w #2,d6
	moveq #15,d7
_pixelKeepWord:

Dessiner à l'économie grâce au Blitter

L'affichage des colonnes au CPU est une tâche intensive pour ce dernier. Il existe un moyen expéditif pour le soulager : recourir au Blitter.
Dans un précédent article, nous avons vu que le Blitter permet de copier des blocs et de tracer des droites. Cette dernière fonctionnalité est particulièrement intéressante en l'espèce, car le Blitter peut tracer une ligne reproduisant un motif de 16 pixels. Ce motif ne pourrait-il pas être la colonne d'un caractère à dessiner ? Certainement. Nous allons donc utiliser le Blitter pour tracer autant de lignes de 16 pixels, verticales et texturées qu'il y a de colonnes du sine scroll à dessiner.
Le dessin d'une colonne au CPU s'effectue de haut en bas, mais le dessin au Blitter doit s'effectuer en sens inverse. En effet, le motif de la colonne est orienté de telle sorte que son bit 15 ne correspond pas au premier pixel de la colonne, mais à son dernier.
Affichage d'une colonne d'un caractère au Blitter
Configurer le Blitter pour tracer des lignes est un peu fastidieux, car il faut stocker des valeurs dans nombre de registres. Une bonne partie de cette initialisation peut être effectuée une fois pour toutes les lignes à tracer. En effet, le contenu des registres concernés n'est pas modifié lors d'un tracé de ligne. Il vaut donc pour toutes les lignes.
Commençons par définir quelques paramètres pour nous y retrouver :
LINE_DX=15	;# de lignes de la droite -1 : LINE_DX=max (abs(15-0),abs(0,0))
LINE_DY=0	;# de colonnes de la droite -1 : LINE_DY=min (abs(15-0),abs(0,0))
LINE_OCTANT=1
Procédons ensuite à la partie récurrente de l'initialisation du Blitter :
	move.w #4*(LINE_DY-LINE_DX),BLTAMOD(a5)
	move.w #4*LINE_DY,BLTBMOD(a5)
	move.w #DISPLAY_DX>>3,BLTCMOD(a5)
	move.w #DISPLAY_DX>>3,BLTDMOD(a5)
	move.w #(4*LINE_DY)-(2*LINE_DX),BLTAPTL(a5)
	move.w #$FFFF,BLTAFWM(a5)
	move.w #$FFFF,BLTALWM(a5)
	move.w #$8000,BLTADAT(a5)
	move.w #(LINE_OCTANT<<2)!$F041,BLTCON1(a5)		;BSH3-0=15, SIGN=1, OVF=0, SUD/SUL/AUL=octant, SING=0, LINE=1
Pour chaque colonne à dessiner, nous devons fournir au Blitter l'adresse du mot contenant le pixel de départ de la droite dans BPLCPTH / BLTCPTL et BLTDPTH / BLTDPTL, le numéro de ce pixel dans BLTCON0 et le motif de la droite dans BLTADAT.
	WAITBLIT
	lea LINE_DX*(DISPLAY_DX>>3)(a4),a4
	move.l a4,BLTCPTH(a5)
	move.l a4,BLTDPTH(a5)
	move.w (a1),BLTBDAT(a5)
	move.w d7,d2
	ror.w #4,d2
	or.w #$0B4A,d2
	move.w d2,BLTCON0(a5)		;ASH3-0=pixel, USEA=1, USEB=0, USEC=1, USED=1, LF7-0=AB+AC=$4A
Une écriture dans BLTSIZE demandant le tracé d'une ligne de 16 pixels permet de lancer le Blitter :
	move.w #((LINE_DX+1)<<6)!$0002,BLTSIZE(a5)
Ce code vient presque se substituer à celui utilisé pour dessiner une colonne au CPU. La seule différence notable est la manière dont cette colonne est identifiée. Avec le CPU, c'est le numéro du bit dans le mot courant qui est utilisé. Avec le Blitter, c'est le numéro du pixel dans ce mot. Ces numérotations sont en sens inverse l'une de l'autre : le pixel 15 correspond au bit 0 ; le pixel 14, au bit 1 ; etc.
Dans le source, les codes des versions au CPU et au Blitter coexistent. Une constante BLITTER permet de basculer de l'une à l'autre pour tester (0 pour dessiner au CPU et 1 pour dessiner au Blitter) :
BLITTER=1
La valeur de cette constante conditionne la compilation de parties du code. Par exemple :
	IFNE BLITTER

	moveq #SCROLL_X,d7
	and.w #$000F,d7

	ELSE
	
	moveq #SCROLL_X,d4
	and.w #$000F,d4
	moveq #15,d7
	sub.b d4,d7

	ENDC
C'est fini ! Le sine scroll s'affiche. Toutefois, il reste à l'enjoliver en rajoutant quelques effets peu coûteux en cycles, et surtout à en optimiser le code...