Cet article est le cinquième et dernier 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 cracktro du groupe Angels :
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 16x16 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 le quatrième article nous avons vu comment enjoliver le sine scroll avec quelques effets peu coûteux en cycle assurés par le Copper.
Dans ce cinquième et dernier article, nous allons optimiser le code afin d'être bien certain de tenir dans la trame, et nous protéger des lamers tentés de modifier le texte. Pour terminer, nous verrons s'il n'y a pas quelques leçons à tirer de cette immersion dans la programmation en assembleur du hardware de l'Amiga.
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...
Mise à jour du 12/07/2017 : Perfomances améliorées suite à la suppression du positionnement de BLTPRI dans DMACON.
Mise à jour du 27/10/2018 : Suite à la "découverte" d'une option oubliée dans WinUAE, rajout d'une section présentant des versions optimisées du sine scroll avec l'étoile.
Click here to read this article in english.
Précalculer pour tenir dans la trame
Notre sine scroll est au pixel, ce qui est mieux que celui du groupe Falon évoqué dans le premier article, mais il ne faut pas oublier que nous l'exécutons sur Amiga 1200 et non sur Amiga 500, c'est-à-dire sur un ordinateur bien plus rapide ! Pour savoir si notre code est performant, nous devons le tester sur un Amiga 500.
Pour cela, nous allons mettre l'exécutable sur disquette et booter à partir de cette dernière dans le contexte d'une émulation d'Amiga 500.
Dans ASM-One, utilisons les commandes en ligne A (Assemble) pour assembler, puis WO (Write Object) pour générer un exécutable et l'enregistrer dans SOURCES: sous le nom de sinescroll.exe. Rendons-nous alors dans le Workbench. Double-cliquons sur l'icône du lecteur DH0, puis sur celle du dossier System et enfin sur celle du Shell.
Pressons F12 pour accéder à la configuration de WinUAE. Dans la rubrique Hardware, cliquons sur Floppy drives. Cliquons sur Create Standard Disk pour créer une disquette formatée au format ADF. Cliquons ensuite sur ... à droite du lecteur DF0: et sélectionnons ce fichier pour simuler l'introduction de la disquette dans le lecteur. Cliquons enfin sur OK pour revenir au Workbench.
Dans le Shell, exécutons cette série de commandes pour commander l'exécution de sinescroll.exe lorsque nous booterons avec la disquette :
install df0: copy sources:sinescroll.exe df0: makedir df0:s echo "sinescroll.exe" > df0:s/Startup-Sequence
L'archive mentionnée au début de cet article contient le fichier ADF qui correspond à la disquette ainsi préparée.
Créons alors une émulation d'Amiga 500 - nous aurons besoin du Kickstart 1.3. La chose faite, insérons la disquette dans le lecteur DF0: et démarrons la simulation en cliquant sur Reset. Le sine scroll se lance automatiquement.
Le résultat tourne tout juste dans la trame - pour ne pas être méchant en disant : pas dans la trame. Difficile de prétendre produire un sine scroll d'aussi belle hauteur que celui de Falon dans ces conditions ? Bah !, nous pourrions recourir à une astuce. Sans la documenter ici, elle consisterait à doubler les lignes à peu de frais, en demandant au Copper de modifier les modulos à chaque ligne afin de répéter la ligne du dessus une ligne sur deux. Le résultat perdrait en finesse, mais il pourrait tromper son monde.
Il resterait toujours à optimiser le code pour tenir dans la trame. Ce dernier ayant été écrit sans réfléchir à la performance, il ne faudrait pas trop se creuser la tête pour trouver les moyens de réaliser de jolis gains de temps.
A cette fin, il faudrait commencer par se référer non seulement au M68000 8-/16-/32-Bit Microprocessors User's Manual, qui détaille le nombre de cycles d'horloge pris par une instruction selon la variante qui en est utilisée, mais aussi à l'Amiga Hardware Reference Manual, qui explique la manière dont le CPU et les différents coprocesseurs disposant d'accès DMA se partagent les cycles d'accès à la mémoire durant le tracé d'une ligne - la belle figure 6-9 du manuel.
Il faudrait ensuite travailler sur l'algorithme pour parvenir à un code performant au regard des consommations de cycles qui viennent d'être évoquées. Comme toujours, le premier réflexe devrait être de chercher à sortir de la boucle principale tout ce qui peut être précalculé, du moment que la mémoire pour stocker des précalculs est disponibles.
Par exemple, il est possible de précalculer l'ordonnée de chaque colonne pour toutes les valeurs de l'angle variant entre 0 et 359 degrés. Ainsi, lors de l'affichage d'une colonne, le code exécuté à chaque itération de la boucle principale n'est plus... :
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 add.w d6,d1 lea (a2,d1.w),a4
...mais :
move.w (a2,d0.w),d4 add.w d2,d4 lea (a0,d4.w),a4
Ou encore, il est possible d'analyser le texte avant la boucle pour créer une liste des colonnes auquel ce texte correspond. Cette fois, c'est une vingtaine de lignes exécutées à chaque itération de la boucle principale qui sont d'un coup remplacées par les quelques suivantes :
cmp.l a1,a3 bne _nextColumnNoLoop movea.l textColumns,a1 _nextColumnNoLoop:
Après avoir épuisé les précalculs, il est possible d'intervenir sur le code. Par exemple, pour supprimer le double test d'attente du Blitter... :
_waitBlitter0\@ btst #14,DMACONR(a5) bne _waitBlitter0\@ _waitBlitter1\@ btst #14,DMACONR(a5) bne _waitBlitter1\@
...ce qui donne :
_waitBlitter0\@ btst #14,DMACONR(a5) bne _waitBlitter0\@
Ou encore, pour stocker à l'avance $0B4A dans le registre de données du CPU (ici, D3) utilisé pour alimenter BLTCON0 lorsqu'une colonne est tracée au Blitter... :
move.w d3,d7 ror.w #4,d7 or.w #$0B4A,d7 move.w d2,BLTCON0(a5)
...ce qui donne (pour passer au pixel suivant, ajouter $1000 à D3 et non plus 1, et tester le drapeau C du registre des conditions internes du CPU par BCC pour détecter un dépassement du 16ème pixel, lequel entraîne une réinitialisation D3 à la valeur voulue $0B4A qu'il est donc inutile de demander !) :
move.w d3,BLTCON0(a5)
Le source de cette version optimisée correspond au fichier sinescroll_final.s qui se trouve dans l'archive mentionnée au début de cet article.
En bonus, ce source contient un code qui détermine le nombre de lignes parcourues par le faisceau d'électrons entre le début et la fin des calculs d'une trame. Ce code affiche ce nombre en décimal en haut à gauche - en PAL, c'est-à-dire à 50Hz, le faisceau d'électrons parcourt 313 lignes. Pour visualiser ce temps pris par les calculs, la couleur 0 est passée en rouge au début de cette période et en vert à sa fin.
Il est ainsi possible de constater que sur Amiga 500, il faut 138 lignes pour afficher le sine scroll dans la trame (à gauche), alors que sur Amiga 1200 (à droite), il en faut seulement 54 :
Le gain généré par cette optimisation est important, mais sans doute plus limité, sur Amiga 1200 où le nombre de lignes passe de 62 à 54, soit un gain de 13% - pour information, le nombre de lignes d'une version où les colonnes sont tracées au CPU, et non au Blitter, passe de 183 à 127 lignes après optimisation, soit un gain de 31% !
Toute économie est toujours bonne à prendre, mais il ne faut pas perdre de vue qu'un précalcul immobilise toujours de la mémoire et génère une attente pour l'utilisateur si le résultat de ce précalcul n'a pas été stocké sous forme de données liées au code dans l'exécutable. En l'occurrence, précalculer les colonnes de la totalité du texte conduit à immobiliser 32 octets par caractère, soit 34 656 octets pour les 1 083 caractères de notre texte. Bon, cela reste raisonnable.
Ainsi, le sine scroll ne tenait pas dans la trame sur Amiga 500. Désormais, il reste largement assez de temps pour l'enjoliver ! Ne nous en privons pas, et sans qu'il soit question de détailler le code que cela implique - le source correspond au fichier sinescroll_star.s qui se trouve dans l'archive mentionnée au début de cet article -, rajoutons pour finir une étoile vectorielle qui tourne dans le fond, avec ombre projetée et reflet dans le miroir comme le sine scroll, ces effets ne coûtant pas plus :
Pour afficher le tout, il faut 219 lignes sur Amiga 500, et 103 sur Amiga 1200, sans aucune optimisation - en particulier, le remplissage n'est pas limité à la zone qu'occupe l'étoile, ce qui fait perdre beaucoup de temps sur Amiga 500. Nous pourrions facilement démultiplier la hauteur du sine scroll en répétant des lignes à l'aide d'un jeu sur le modulo au Copper, rajouter un starfield à base de sprites hardware répétés au Copper, agrémenter l'effet avec un beau module de Monty, etc. Mais c'est une autre histoire...