Coder un sine scroll sur Amiga (4/5)

Mise à jour du 17/07/2017 : attente du Blitter avant de procéder à la finalisation.
Cet article est le quatrième article 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 intro, aussi magnifique que vintage, du groupe Miracle :
Sine scroll dans une intro du groupe Miracle
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. Enfin, dans le troisième article, nous avons vu comment dessiner et animer le sine scroll, d’abord au CPU, puis au Blitter.
Dans ce quatrième article, nous allons enjoliver le sine scroll avec quelques effets peu coûteux en cycles car assurés par le Copper, et rendre la main aussi proprement que possible à l’OS.
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…

Ajouter ombre et miroir grâce au Copper

Qu’est-ce qu’une ombre portée au sud-est, sinon un bitplane qu’on affiche par-dessous lui-même en le décalant légèrement sur la droite et vers le bas ? Et qu’est-ce qu’un miroir, sinon un bitplane qu’on continue d’afficher à partir d’une certaine ligne, mais en remontant plutôt qu’en descendant ligne à ligne dans ce dernier ?
Exposés ainsi, les effets d’ombre de miroir semble triviaux. Facile à dire ? Facile à faire ! grâce au Copper, qui permet très simplement de modifier au début de n’importe quelle ligne l’adresse à laquelle le hardware doit lire les données du bitplane et le retard avec lequel il doit les afficher.
Comme d’habitude, commençons par définir les paramètres des effets à produire :
  • SHADOW_DX et SHADOW_DY correspondent à l’ampleur de l’ombre vers la droite et vers le bas, respectivement, et SHADOW_COLOR, à la couleur de cette dernière ;
  • MIRROR_Y correspond à l’ordonnée à laquelle le miroir débute, et MIRROR_COLOR et MIRROR_SCROLL_COLOR, à la couleur du fond et à la couleur du scroll dans le miroir, respectivement.
SHADOW_DX=2	;Compris entre 0 et 15
SHADOW_DY=2
SHADOW_COLOR=$0777
MIRROR_Y=SCROLL_Y+SCROLL_DY
MIRROR_COLOR=$000A
MIRROR_SCROLL_COLOR=$000F
Essayons alors d’y voir plus clair dans la manière dont la Copper list doit se présenter. L’expérience montre que plutôt que de se lancer tête baissée dans son écriture, il vaut mieux schématiser le déroulement des opérations ligne à ligne – le Copper permet d’effectuer des MOVE en cours de ligne, mais ce ne sera pas utile ici. Pour ne pas surcharger le schéma, la mention à une constante entre crochets signifie qu’il faut lui ajouter DISPLAY_Y.
WAIT et MOVE pour l'ombre et le miroir
L’ombre tout d’abord. Le hardware lit les données de la ligne du bitplane à afficher à l’adresse 32 bits figurant dans les registres 16 bits BLT1PTH et BPL1PTL, qu’il incrémente tandis qu’il progresse le long de la ligne. A la fin de la ligne, avant de débuter la suivante, il ajoute BPL1MOD à ces registres pour obtenir l’adresse à laquelle il commence à lire les données de la ligne suivante.
Jusqu’à présent, l’affichage se résumait à un bitplane, le bitplane 1. Nous rajoutons un bitplane 2, en indiquant au hardware que les données de ce second bitplane sont les mêmes que celle du premier :
	move.l bitplaneA,d0
	move.w #BPL1PTL,(a0)+
	move.w d0,(a0)+
	move.w #BPL2PTL,(a0)+
	move.w d0,(a0)+
	swap d0
	move.w #BPL1PTH,(a0)+
	move.w d0,(a0)+
	move.w #BPL2PTH,(a0)+
	move.w d0,(a0)+
L’ajout d’un bitplane entraîne quelques modifications supplémentaires à la Coper list :
  • spécifier le modulo des bitplanes pairs, et non plus seulement celui des bitplanes impairs :
    	move.w #BPL2MOD,(a0)+
    	move.w #0,(a0)+
    
  • spécifier deux couleurs supplémentaires, car la palette est étendue à 4 couleurs :
    	move.w #COLOR02,(a0)+
    	move.w #SCROLL_COLOR,(a0)+
    	move.w #COLOR03,(a0)+
    	move.w #SCROLL_COLOR,(a0)+
    
En passant DISPLAY_DEPTH à 2, nous modifions incidemment la valeur que le Copper stocke dans BPLCON0 pour indiquer le nombre de bitplanes, donc rien à modifier ici.
Jusqu’à la ligne [SCROLL_Y+SHADOW_DY-1], les deux bitplanes sont superposés. Le sine scroll est donc affiché avec le couleur 3, ce qui explique pourquoi SCROLL_COLOR est stocké dans COLOR03.
A partir de [SCROLL_Y+SHADOW_DY], les deux bitplanes sont décalés :
  • Horizontalement, en passant à SHADOW_DX dans BPLCON1 la valeur du retard avec lequel le hardware affiche les bitplanes pairs. Cette modification doit être demandée au Copper au début de la ligne [SCROLL_Y+SHADOW_DY]. Noter que SHADOW_DX ne peut dépasser 15 ; au-delà, il faut jouer sur BPL2MOD et BPLCON1 simultanément.
  • Verticalement, en passant à [-SHADOW_DY*(DISPLAY_DX>>3)] le modulo des bitplanes pairs dans BPL2MOD. Cette modification doit être demandée au Copper au début de la ligne [SCROLL_Y+SHADOW_DY -1] pour affecter la ligne suivante, comme expliqué plus tôt.
Décalage des bitplanes pour générer l'ombre
A la ligne [SCROLL_Y+SHADOW_DY], l’adresse de la ligne du bitplane 2 devient celle du bitplane 1 moins SHADOW_DY lignes. Par la suite, il est nécessaire que l’affichage du bitplane 2 se poursuive normalement, et c’est pourquoi BPL2MOD doit repasser à 0 au début de la ligne [SCROLL_Y+SHADOW_DY]. A défaut, la ligne répétée le serait indéfiniment.
Le miroir ensuite. Ainsi, BPLxMOD peut être mis à profit pour demander au hardware de revenir en arrière pour répéter l’affichage de bitplanes à partir d’une certaine ligne. Si DISPLAY_DX correspond à la largeur des bitplanes, alors :
  • au début de la ligne [MIRROR_Y-1], passer le modulo à -(DISPLAY_DX>>3) permettra de répéter la ligne [MIRROR_Y-1] qui va être tracée à la ligne suivante [MIRROR_Y] ;
  • au début de la ligne [MIRROR_Y], passer le modulo à -2*(DISPLAY_DX>>3) permettra de répéter la ligne [MIRROR_Y-2] déjà tracée à la ligne suivante [MIRROR_Y+1], produisant un effet de miroir ;
  • tant que ce modulo sera maintenu, il permettra de répéter à la ligne y la ligne tracée en 2*MIRROR_Y-1-y, ce qui perpétuera l’effet de miroir.
    Après avoir choisi de faire coïncider le début de l’effet de miroir avec la fin de l’effet d’ombre, il nous reste à modifier les couleurs 0 et 3 via COLOR00 et COLOR03 au début de la ligne [MIRROR_Y] pour que le sine scroll semble se refléter dans un autre milieu.
Répétition de lignes déjà tracées pour générer le miroir
Tous les MOVE que le Copper doit accomplir pour réaliser ce qui vient d’être décrit ne doivent se produire qu’au tout début de certaines lignes. Ces instructions sont donc précédées de WAIT qui instruisent le Copper d’attendre que le faisceau d’électron a atteint ou dépassé certaines lignes.
Pour rappel, une instruction WAIT portant sur une position (x, y) à l’écran prend la forme de deux mots, un mot (y<<8)!((x>>2)<<1)!$0001 suivi d’un autre servant de masque pour indiquer au Copper sur quels bits des coordonnées la comparaison entre la position indiquée et celle du faisceau d’électrons doit porter.
En l’espèce, nous souhaitons que le Copper se contente de comparer des ordonnées. Aussi tous nos WAIT prennent-ils la forme d’un mot (y<<8)!$0001 suivi d’un mot $FF00.
Nous commençons donc par le décalage vertical de l’ombre… :
	move.w #((DISPLAY_Y+SCROLL_Y+SHADOW_DY-1)<<8)!$0001,(a0)+
	move.w #$FF00,(a0)+
	move.w #BPL2MOD,(a0)+
	move.w #-SHADOW_DY*(DISPLAY_DX>>3),(a0)+
…suivi du décalage horizontal… :
	move.w #((DISPLAY_Y+SCROLL_Y+SHADOW_DY)<<8)!$0001,(a0)+
	move.w #$FF00,(a0)+
	move.w #BPL2MOD,(a0)+
	move.w #0,(a0)+
	move.w #BPLCON1,(a0)+
	move.w #SHADOW_DX<<4,(a0)+
...suivi du de la fin du décalage vertical de l'ombre et du démarrage du miroir... :
	move.w #((DISPLAY_Y+MIRROR_Y-1)<<8)!$0001,(a0)+
	move.w #$FF00,(a0)+
	move.w #BPL1MOD,(a0)+
	move.w #-(DISPLAY_DX>>3),(a0)+
	move.w #BPL2MOD,(a0)+
	move.w #(SHADOW_DY-1)*(DISPLAY_DX>>3),(a0)+
...suivi de la fin du décalage horizontal de l'ombre et de la répétition des lignes dans le miroir ainsi que de la palette qui s'applique dans ce dernier :
	move.w #((DISPLAY_Y+MIRROR_Y)<<8)!$0001,(a0)+
	move.w #$FF00,(a0)+
	move.w #BPLCON1,(a0)+
	move.w #$0000,(a0)+
	move.w #BPL1MOD,(a0)+
	move.w #-(DISPLAY_DX>>2),(a0)+
	move.w #BPL2MOD,(a0)+
	move.w #-(DISPLAY_DX>>2),(a0)+
	move.w #COLOR00,(a0)+
	move.w #MIRROR_COLOR,(a0)+
	move.w #COLOR03,(a0)+
	move.w #MIRROR_SCROLL_COLOR,(a0)+

Rendre la main à l'OS

Quand l'utilisateur clique sur le bouton de la souris, nous devons rendre la main à l'OS proprement - du moins, aussi proprement que possible, car rien ne garantit qu'il se remette de ce que nous lui avons fait subir en le coupant aussi brutalement du hardware.
Pour commencer, nous nous assurons que le Blitter n’est pas en train de travailler :
	WAITBLIT
Ensuite, nous coupons les interruptions et les canaux DMA... :
	move.w #$7FFF,INTENA(a5)
	move.w #$7FFF,INTREQ(a5)
	move.w #$07FF,DMACON(a5)
...et nous les réactivons après les avoir restaurés dans l'état dans lequel nous les avions trouvés :
	move.w dmacon,d0
	bset #15,d0
	move.w d0,DMACON(a5)
	move.w intreq,d0
	bset #15,d0
	move.w d0,INTREQ(a5)
	move.w intena,d0
	bset #15,d0
	move.w d0,INTENA(a5)
Nous rétablissons alors la Copper list du Workbench. Son adresse réside à un certain offset de l'adresse de base de la bibliothèque Graphics. Pour y accéder, nous ouvrons cette librairie par une appel à la fonction OldOpenLib() d'Exec. Une fois l'adresse de la Copper list récupérée, nous demandons au Copper de l'utiliser désormais :
	lea graphicslibrary,a1
	movea.l $4,a6
	jsr -408(a6)
	move.l d0,a1
	move.l 38(a1),COP1LCH(a5)
	clr.w COPJMP1(a5)
	jsr -414(a6)
Nous rétablissons le fonctionnement normal du système par un appel à la fonction Permit() d'Exec :
	movea.l $4,a6
	jsr -138(a6)
Nous libérons les espaces alloués en mémoire par autant d'appels à la fonction FreeMem() d'Exec :
	movea.l font16,a1
	move.l #256<<5,d0
	movea.l $4,a6
	jsr -210(a6)
	movea.l bitplaneA,a1
	move.l #(DISPLAY_DX*DISPLAY_DY)>>3,d0
	movea.l $4,a6
	jsr -210(a6)
	movea.l bitplaneB,a1
	move.l #(DISPLAY_DX*DISPLAY_DY)>>3,d0
	movea.l $4,a6
	jsr -210(a6)
	movea.l bitplaneC,a1
	move.l #(DISPLAY_DX*DISPLAY_DY)>>3,d0
	movea.l $4,a6
	jsr -210(a6)
	movea.l copperlist,a1
	move.l #COPSIZE,d0
	movea.l $4,a6
	jsr -210(a6)
De là, il ne nous reste plus qu'à dépiler les registres et à rendre la main :
	movem.l (sp)+,d0-d7/a0-a6
	rts
Reste à savoir si tout cela tient dans la trame, et à optimiser le code si tel n'est pas le cas...
Coder un sine scroll sur Amiga (4/5)