Obligement - L'Amiga au maximum

Vendredi 23 mai 2025 - 18:13  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

Actualité (récente)
Actualité (archive)
Comparatifs
Dossiers
Entrevues
Matériel (tests)
Matériel (bidouilles)
Points de vue
En pratique
Programmation
Reportages
Quizz
Tests de jeux
Tests de logiciels
Tests de compilations
Trucs et astuces
Articles divers

Articles in English


Réseaux sociaux

Suivez-nous sur X




Liste des jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z,
ALL


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Associations
Jeux
Logiciels
Matériel
Magazines et médias
Pages personnelles
Réparateurs
Revendeurs
Scène démo
Sites de téléchargement
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


Programmation : Assembleur - Programmer un défilement sinusoïdal ("sine scroll") sur Amiga (partie 5)
(Article écrit par Yragael et extrait de www.stashofcode.fr - juillet 2017, mis à jour en octobre 2018)


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 codeurs de démos et autres cracktros durant un temps. Par exemple, dans cette cracktro du groupe Angels :

Programmer un défilement sinusoïdal
Défilement sinusoïdal une 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 programmer la liste Copper 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 tampon mémoire pour alterner proprement les images à l'écran. Puis, dans le troisième article, nous avons vu comment dessiner et animer le défilement sinusoïdal, d'abord au processeur, puis au Blitter. Dans le quatrième article, nous avons vu comment enjoliver le défilement sinusoïdal 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 minables ("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 matériel de l'Amiga.

Cliquez ici pour télécharger l'archive contenant le code et les données du programme présenté dans cet article - 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 défilement sinusoïdal avec l'étoile.
Précalculer pour tenir dans la trame

Notre défilement sinusoïdal 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 démarrer à 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 "Matériel" (Hardware), cliquons sur "Lecteurs de disquette" (Floppy drives). Cliquons sur "Créer disquette standard" (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 démarrerons 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 "Réinitialisation" (Reset). Le défilement sinusoïdal 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 défilement sinusoïdal 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 processeur 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 processeur (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 processeur par BCC pour détecter un dépassement du 16e 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 à 50 Hz, 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 défilement sinusoïdal dans la trame (à gauche), alors que sur Amiga 1200 (à droite), il en faut seulement 54 :

Programmer un défilement sinusoïdal Programmer un défilement sinusoïdal
Temps par trame pris par la version optimisée sur Amiga 500 et 1200

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 processeur, 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 1083 caractères de notre texte. Bon, cela reste raisonnable.

Ainsi, le défilement sinusoïdal 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 défilement sinusoïdal, ces effets ne coûtant pas plus :

Programmer un défilement sinusoïdal
Avec une animation vectorielle, c'est mieux...

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 défilement sinusoïdal en répétant des lignes à l'aide d'un jeu sur le modulo au Copper, rajouter un champ d'étoiles à base de sprites matériels répétés au Copper, agrémenter l'effet avec un beau module de Monty, etc. Mais c'est une autre histoire...

Se protéger des minables ("lamers")

Un minable pourrait reprendre notre beau défilement sinusoïdal ! En particulier, il pourrait utiliser un éditeur hexadécimal pour modifier le texte qui défile. Pour se protéger de ce minable, adoptons une protection de base dont il fera les frais s'il entreprend de s'y attaquer.

Pour que le texte ne soit pas apparent, nous devons l'encoder. Contentons-nous de combiner les octets de ses caractères par XOR avec TEXT_XOR, un octet de valeur quelconque. Ainsi, les caractères n'apparaissent pas comme des caractères dans un éditeur hexadécimal.

Et si tout de même le minable devait deviner l'opération - ce dont nous lui laissons délibérément la possibilité en exposant le texte encodé à une attaque fondée sur l'analyse de récurrences -, calculons TEXT_CHECKSUM, une somme de contrôle du texte encodé, et rajoutons ici et là des appels à un code qui vérifie que le texte n'a pas été altéré. Ce code calcule la somme de contrôle du texte courant et le remplace par "You are a LAMER!" (de la somme de contrôle TEXT_CHECKSUM_LAMER) si cette somme de contrôle ne correspond à aucune des sommes de contrôles de nos désormais deux textes originaux :

Programmer un défilement sinusoïdal
La punition du minable qui modifierait le texte du défilement sinusoïdal

Ne factorisons pas ce code, mais répétons-le, pour en demander l'exécution en divers endroits de sorte que le minable ne puisse s'en débarrasser en substituant simplement un RTS à la première instruction de ce qui constituerait autrement son unique occurrence.

;Attention au contexte dans lequel la macro est utilisée, car elle peut modifier
;la longueur du texte initial (qui doit être au moins aussi long que "You are a LAMER!"
;sous peine d'écraser des données) et donc embrouiller le code qui était en train de le parcourir

CHECKTEXT:	MACRO
	movem.l d0-d1/a0-a1,-(sp)
	lea text,a0
	clr.l d0
	clr.l d1
_checkTextLoop\@
	move.b (a0)+,d0
	add.l d0,d1
	eor.b #TEXT_XOR,d0
	bne _checkTextLoop\@
	cmp.l textChecksum,d1
	beq _checkTextOK\@
	move.l #TEXT_CHECKSUM_LAMER,textChecksum
	lea text,a0
	lea textLamer,a1
_checkTextLamerLoop\@
	move.b (a1)+,d0
	move.b d0,(a0)+
	eor.b #TEXT_XOR,d0
	bne _checkTextLamerLoop\@
_checkTextOK\@
	movem.l (sp)+,d0-d1/a0-a1
	ENDM

Très "easter egg". On est jeune, on rigole.

Quelques mots pour conclure

Programmer en assembleur 68000 est un travail exigeant. Le nombre important de registres disponibles et le souci permanent d'en optimiser l'usage conduit le codeur à empiler et dépiler dans sa propre mémoire l'usage qu'il en fait tandis qu'il progresse dans l'écriture du code. Dans mon souvenir, celui qui code en assembleur 80x86 est moins confronté à cette exigence, car le nombre des registres est si faible et leurs usages tellement contraints qu'il est indispensable de s'appuyer sans cesse sur la pile du processeur, pile dont il est plus facile de se souvenir du contenu que de celui de treize registres.

Je n'étais pas parti dans l'idée de me remettre à programmer sur Amiga lorsque j'ai entrepris de revisiter le code d'une cracktro dans la série d'articles précédents. C'est en relisant l'Amiga Hardware Reference Manual que je me suis rappelé que je n'avais jamais poussé bien loin l'étude de la manière dont le Blitter trace des lignes, fonctionnalité que je savais être utilisée pour produire notamment un défilement sinusoïdal. Finalement, j'ai voulu clarifier les choses, et j'ai programmé à partir de rien cet effet.

Plus généralement, en feuilletant ce manuel mais aussi ceux du 68000, j'ai pu constater combien j'étais resté sur une vision très superficielle du fonctionnement du matériel et du processeur à l'époque. Si j'ai donc une leçon à formuler, c'est que chaque fois qu'on s'intéresse à une technologie, il faut se donner la peine de lire scrupuleusement l'intégralité de sa documentation de référence plutôt que de se contenter, par pure fainéantise, de s'en remettre à son intuition.

C'est qu'à ce régime, on prend non seulement le risque de manquer des fonctionnalités importantes, mais de plus, celui d'en mal comprendre certaines. Par exemple :

	btst #14,$dff002

De prime abord, cette instruction teste le bit 14 du mot se trouvant à l'adresse $DFF002. En fait, la lecture de la description de BTST dans le M68000 Family Programmer's User Manual révèle que lorsque le premier opérande est N et le second est une adresse, c'est le bit N%8 (c'est-à-dire : N modulo 8) de l'octet se trouvant à l'adresse qui est testé. En l'espèce, c'est donc le bit 14%8=6 de l'octet se trouvant à l'adresse $DFF002 qui est testé. Cela correspond bien au bit 14 de l'octet de poids fort du mot se trouvant à cette adresse, si bien que notre intuition se révèle pertinente. Toutefois, c'est par chance. Présumer ainsi de certains fonctionnements peut générer des erreurs d'autant plus difficiles à corriger qu'on est loin de soupçonner où elles se logent.

La lecture de la documentation de référence s'impose donc toujours comme un préalable difficilement contournable pour qui souhaite maîtriser véritablement une technologie. Et je dis bien la documentation de référence dans le texte, et non une de ses formes vulgarisées. C'est qu'au prétexte de rendre un savoir accessible, la vulgarisation prend trop souvent des libertés avec ce dernier, empruntant des raccourcis et faisant des impasses qui ne font que fourvoyer le talent et encourager la médiocrité. Une forme vulgarisée d'une documentation de référence ne doit jamais être considérée que comme un point d'entrée sur cette dernière. Elle ne saurait dispenser d'au moins en tenter la lecture, quand bien même cette entreprise peut se révéler ardue. C'est que la tendance est malheureusement plus aux exposés formels que didactiques - les auteurs des spécifications des technologies du Web auraient tout à gagner à lire l'Amiga Hardware Reference Manual !

Ce sera tout pour cette fois, et sans doute pour toujours en ce qui concerne la programmation en assembleur de l'Amiga - à laquelle je ne m'étais pas adonné depuis bientôt un quart de siècle. Je dédie ce travail à un vieux pote, Stormtrooper, sans la motivation duquel je n'aurais jamais entrepris de me mettre au "metal bashing" à l'époque, et à tous ceux dont les pseudos défilent dans les inévitables salutations que pourront lire les courageux qui assembleront le source de ce défilement sinusoïdal. "Amiga rulez!"

Optimiser pour être "cycle-exact"

A l'occasion de la production d'une craktro récemment, il m'est apparu que j'avais oublié d'activer une option de WinUAE permettant une émulation exacte du matériel. Il s'agit de l'option "Cycle-exact (full)", dont l'activation entraîne celle de l'option "Cycle-exact (DMA/Memory access)" :

Programmer un défilement sinusoïdal
"Cycle-exact", la petite option qui vous perdra...

L'activation de ces options est indispensable pour espérer programmer fidèlement pour tout Amiga. A défaut, le processeur émulé dispose de beaucoup plus de cycles qu'il n'en dispose en réalité, car il ne se fait pas voler des cycles par le DMA. Autrement dit, le résultat observé dans WinUAE a toutes les chances d'être bien plus rapide qu'il ne le serait sur un Amiga bien réel.

C'est ce que j'ai pu constater dans le cas du défilement sinusoïdal, fort heureusement uniquement quand l'étoile est rajoutée. Sur Amiga 1200, j'ai corrigé ce problème de la manière suivante :
  • En limitant la zone remplie au Blitter dans le plan de bits de l'étoile au rectangle englobant cette dernière.
  • En limitant la zone effacée dans le plan de bits du défilement sinusoïdal à la bande occupée par ce dernier, et en l'effaçant au processeur pendant que le Blitter remplit le plan de bits de l'étoile.
  • En limitant la zone effacée dans le plan de bits de l'étoile au rectangle englobant cette dernière, et en l'effaçant aussi processeur pendant que le Blitter est toujours en train de remplir le plan de bits de l'étoile.
Cliquez ici pour récupérer le source. Le temps pris par une itération de la boucle principale s'évalue alors à 240 lignes, ce qui laisse assez de marge pour rajouter de la musique.

Sur Amiga 500, cette optimisation par la parallélisation ne permet toujours pas de tenir dans la trame. La seule solution est donc de précalculer les images de l'étoile, et de copier l'image courante dans le plan de bits de l'étoile au Blitter. Cette dernière étant un motif périodique, il est possible de se contenter de précalculer 360/5=72 images. Ce nombre est à diviser par la vitesse de rotation de l'image, sous condition que 72 en soit un multiple.

Cliquez ici pour récupérer le source. Le temps pris par une itération de la boucle principale s'évalue alors à 242 lignes - ce qui comprend aussi l'effet d'une limitation la zone effacée au Blitter dans le plan de bits du défilement sinusoïdal à la bande occupée par ce dernier -, ce qui laisse assez de marge pour rajouter de la musique. Bien évidemment, cette version tourne encore plus rapidement que la précédente sur Amiga 1200, puisqu'elle le temps pris par une itération de la boucle principale est alors réduit à 183 lignes :

Programmer un défilement sinusoïdal Programmer un défilement sinusoïdal
Temps par trame pris par la version avec étoile optimisée sur Amiga 500 et sur Amiga 1200

Une conséquence de la "découverte" de l'option, c'est que les temps indiqués plus haut dans l'article sont donc faux. Le défilement sinusoïdal tient bien dans la trame sur Amiga 500 et Amiga 1200, mais une itération de la boucle principale prend nettement plus de temps que ce qui avait été indiqué, à savoir 183 lignes sur Amiga 500 et 136 lignes sur Amiga 1200 :

Programmer un défilement sinusoïdal Programmer un défilement sinusoïdal
Temps par trame pris réellement par la version optimisée sur Amiga 500 et Amiga 1200

Ainsi, tout est remis en ordre ! Toutes mes confuses.


[Retour en haut] / [Retour aux articles] [Article précédent]