Afficher des sprites et des BOBs sur Amiga OCS et AGA (1/2)

Quoi de plus confortable qu'un sprite ? Le coprocesseur graphique l'affiche en laissant voir le décor derrière ses pixels transparents, et il préserve le décor derrière ses autres pixels pour le restaurer quand le sprite est déplacé. Par ailleurs, il découpe le sprite s'il sort de l'écran.
Malheureusement, les capacités de l'Amiga 500 sont en la matière très limitées. Huit sprites de 16 pixels de large, même de hauteur infinie, en 4 couleurs dont une transparente, seulement ? C'est la dèche...
Les sprites n'en conservent pas moins une certaine utilité, à condition d'en utiliser pleinement le potentiel. Ainsi, il est possible de les attacher pour former jusqu'à un bitmap de 64 pixels de large, de hauteur infinie, en 16 couleurs dont une transparente, notamment. Ou alors, il est possible d'en réutiliser pour créer un playfield supplémentaire, dont le contenu devra toutefois être un motif répétitif qui, dans le meilleur des cas, occupera 48 pixels de large sur une hauteur infinie, en peu de couleurs. Par ailleurs, l'Advanced Graphics Architecture, dont l'Amiga 1200 est doté, dispose de fonctionnalités un peu étendues en matière de sprites. En particulier, leur largeur unitaire passe de 16 à 64 pixels.
Des sprites un peu plus mieux en AGA
Comment afficher des sprites ? Et utiliser les sprites pour afficher un gros et beau bitmap, ou un playfield supplémentaire ? Et enfin utiliser les sprites sur AGA, sachant que Commodore n'a jamais documenté ce hardware ? Tout cela et plus en encore dans ce premier article consacré à l'affichage de bitmaps sur Amiga OCS et AGA.
Mise à jour du 08/07/2018 : Correction d'un bogue dans triplePlayfield.s.
Mise à jour du 01/10/2018 : Tous les sources ont été modifiés pour intégrer une section "StingRay's stuff" qui permet d'assurer le bon fonctionnement sur tous les modèles d'Amiga, notamment dotés d'une carte graphique.
Cliquez ici pour télécharger l'archive contenant le code et les données des programmes présentés ici.
Cette archive contient plusieurs sources :
  • spriteCPU.s pour le simple affichage d'un sprite au CPU (ie : sans DMA) ;
  • sprites.s pour le simple affichage et le déplacement d'un sprite ;
  • sprites16.s pour le simple affichage et le déplacement d'un sprite en 16 couleurs ;
  • spritesField.s pour la réutilisatoin de sprites comme pour un fond étoilé ;
  • spritesCollision.s pour la détection de collisions entre deux sprites, et entre ces sprites et un bitplane ;
  • triplePlayfield.s pour l'affichage d'une plan de sprites (avec dual-playfield tant qu'on y est) ;
  • spritesAGA.s pour l'affichage de sprites exploitant le chipset AA.
NB : Cet article se lit mieux en écoutant l'excellent module composé par Spirit / LSD pour le diskmag Graphevine #14, mais c'est affaire de goût personnel...

Le B-A-BA : afficher un sprite, pour commencer

Cet article présume une certaine familiarité, pour ne pas dire une familiarité certaine, avec la programmation en assembleur du hardware de l'Amiga. Pour s'initier à cette dernière, et notamment installer un environnement de développement, le mieux est se reporter à la série d'articles publiés ici, qui portent sur la programmation d'un sine scroll : 1, 2, 3, 4, et 5.
L'Amiga dispose d'un coprocesseur graphique nommé Copper. Le Copper exécute une série d'instructions à chaque rafraîchissement de l'écran. Ces instructions sont notamment des écritures dans des registres du hardware, dont certains contrôles les sprites.
Le hardware peut afficher huit sprites de 16 pixels de large en 4 couleurs dont une couleur transparente, sur une hauteur illimitée. Les sprites sont couplés (le 0 avec le 1, le 2 avec le 3, etc.). Dans un couple, les sprites partagent la même palette de 4 couleurs, qui n'est qu'un sous-ensemble de la palette de 32 couleurs utilisée pour afficher des pixels à l'écran. Dans ces conditions, la structure de cette dernière palette est la suivante (la mention aux playfields sera expliquée plus tard) :
Couleurs Usage
00 à 07 Playfield 1 (bitplanes 1, 3 et 5)
08 à 15 Playfield 2 (bitplanes 2, 4 et 6)
16 à 19 Sprites 0 et 1
20 à 23 Sprites 2 et 3
24 à 27 Sprites 4 et 5
28 à 31 Sprites 6 et 7
Un sprite est intégralement décrit par une suite de mots. Les deux premiers sont des mots de contrôle, les suivants sont des mots décrivant ligne par ligne de petits bitplanes, et les derniers sont des 0. Par exemple, pour un sprite de deux lignes :
sprite:		DC.W $444C, $5401		;Mots de contrôle
			DC.W $5555, $3333		;Première ligne de 16 pixels
			DC.W $1234, $5678		;Seconde ligne de 16 pixels
			DC.W 0, 0				;Fin
Pour chaque ligne du sprite, les deux mots sont combinés entre eux pour en déduire les indices des couleurs des 16 pixels de la ligne. Pour poursuivre sur cet exemple, le résultat pour la première ligne est celui représenté ici :
Combinaison des mots d'une ligne de données du sprite 0
Ainsi qu'il est possible de le constater, le premier mot de la ligne fournit les bits 0 de la ligne, et le second mot en fournit les bits 1.
Les coordonnées (X, Y) et la hauteur d'un sprite (déduite de l'ordonnée Y + DY de la ligne qui suit la dernière ligne du sprite) sont codées dans les mots de contrôle de manière assez exotique (le bit 7 du second mot est réservé pour l'attachement, ce dont il sera question plus loin). Par exemple, pour afficher un sprite en haut à gauche de l'écran qui débute en classiquement en ($81, $2C) :
Codage des mots de contrôle d'un sprite de DY pixels de hauteur affiché en (X, Y)
Oui, contrairement à ce que prétend l'Amiga Hardware Reference Manual, il faut bien coder X - 1 et non X dans les mots de contrôle. C'est une erreur de la documentation.
Concrètement, il faut donc coder les mots de contrôle ainsi (dans ASM-One, le point d'exclamation est un OR, >> est un décalage non signé sur la droite) :
sprite:			DC.W ((Y&$FF)<<8)!(((X-1)&$1FE)>>1)
				DC.W (((Y+DY)&$FF)<<8)!((Y&$100)>>6)!(((Y+DY)&$100)>>7)!((X-1)&$1)
Pour s'éviter de tels calculs chaque fois qu'il faut déplacer un sprite, une technique consiste à précalculer des mots à combiner pour chacune des 320 positions horizontales, d'une part, et chacune des positions verticales d'autre part. Pour en savoir plus, référez-vous à ce fil d'un forum de l'English Amiga Board.
La suite des mots constituant les données d'un sprite étant écrite, il suffit d'en communiquer l'adresse (qui doit être paire) au hardware pour qu'il affiche le sprite. Chaque sprite dispose de registres SPRxPTH et SPRxPTL pour cela. Par exemple :
	lea #dff000,a5
	move.l #sprite,d0
	move.w d0,SPR0PTL(a5)			;$DFF122
	swap d0
	move.w d0,SPR0PTH(a5)			;$DFF120
Dans les faits, les adresses des sprites ne sont jamais communiquées ainsi par le CPU. Des instructions MOVE sont rajoutées dans la Copper list pour que le Copper les communique à chaque trame :
	lea copperList,a0
	;... (début de la Copper list)
	move.l #sprite,d0
	move.w #SPR0PTL,(a0)+
	move.w d0,(a0)+
	swap d0
	move.w #SPR0PTH,(a0)+
	move.w d0,(a0)+
	;... (suite de la Copper list)
Les sprites ne sont affichés que si le hardware peut accéder à leurs données, ce pour quoi il bénéficie d'un accès direct en mémoire (DMA). L'usage du DMA n'est pas incontournable – il est possible de s'y substituer en écrivant au CPU dans les divers registres où le DMA écrit les données des sprites pour les communiquer au hardware : SPRxPOS, SPRxCTRL, SPRxDATB et SPRxDATA. Le programme spriteCPU.s procède ainsi :
	;Attendre la première ligne du sprite pour commencer à l'afficher

_waitSpriteStart:
	move.l VPOSR(a5),d0
	lsr.l #8,d0
	and.w #$01FF,d0
	cmpi.w #SPRITE_Y,d0
	blt _waitSpriteStart

	;Afficher le sprite. Il faut écrire dans SPRxDATA en dernier, car c'est le moyen de déclencher l'affichage du sprite.

	move.w #((SPRITE_Y&$FF)<<8)!(((SPRITE_X-1)&$1FE)>>1),SPR0POS(a5)
	move.w #(((SPRITE_Y+SPRITE_DY)&$FF)<<8)!((SPRITE_Y&$100)>>6)!(((SPRITE_Y+SPRITE_DY)&$100)>>7)!((SPRITE_X-1)&$1),SPR0CTL(a5)
	move.w #$0F0F,SPR0DATB(a5)
	move.w #$00FF,SPR0DATA(a5)

	;Attendre la ligne de milieu du sprite pour le repositionner horizontalement (8 pixels plus à droite) et modifier ses données

_waitSpriteMiddle:
	move.l VPOSR(a5),d0
	lsr.l #8,d0
	and.w #$01FF,d0
	cmpi.w #SPRITE_Y+(SPRITE_DY>>1),d0
	blt _waitSpriteMiddle
	move.w #((SPRITE_Y&$FF)<<8)!(((SPRITE_X+8-1)&$1FE)>>1),SPR0POS(a5)

	;Ecrire dans SPRxCTL arrête l'affichage du sprite, ce qui interdit de repositionner le sprite horizontalement à la précision d'un pixel, à moins de le réarmer par une écriture dans SPRxDATA, car le bit 0 de cette position est dans SPRxCTL. Autrement dit, les trois lignes qui suivent ne sont nécessaires que si la nouvelle position horizontale est impaire.

	move.w #(((SPRITE_Y+SPRITE_DY)&$FF)<<8)!((SPRITE_Y&$100)>>6)!(((SPRITE_Y+SPRITE_DY)&$100)>>7)!((SPRITE_X+9-1)&$1),SPR0CTL(a5)
	move.w #$F0F0,SPR0DATB(a5)
	move.w #$FF00,SPR0DATA(a5)

	;Attendre la dernière ligne du sprite pour cesser de l'afficher en écrivant n'importe quoi dans SPRxCTL.

_waitSpriteEnd:
	move.l VPOSR(a5),d0
	lsr.l #8,d0
	and.w #$01FF,d0
	cmpi.w #SPRITE_Y+SPRITE_DY,d0
	blt _waitSpriteEnd
	move.w #$0000,SPR0CTL(a5)
Toutefois, cette technique présente peu d'intérêt, sinon aucun. C'est que pour afficher un sprite, il faudrait donc écrire une boucle qui attendrait le faisceau d'électron à chaque ligne à laquelle une ligne du sprite devrait être affichée avant de modifier le contenu des registres SPRxDATB et SPRxDATA avec les données de la nouvelle ligne du sprite, ce qui serait extrêmement contraignant.
Partant, le DMA n'est pas émulé, mais utilisé. Pour afficher les sprites, il est donc nécessaire d'activer aux moins les canaux DMA du Copper, des bitplanes et des sprites :
	move.w #$83A0,DMACON(a5)		;DMAEN=1, BPLEN=1, COPEN=1, SPREN=1
Comme cela peut le suggérer, il n'existe pas de mécanisme pour activer sélectivement tel ou tel des huit sprites. Pour ne pas afficher un sprite, il faut spécifier au hardware que la hauteur du sprite est nulle, ce qui s'effectue en faisant pointer ses données sur des mots de contrôle à 0 :
spriteVoid:		DC.W 0, 0
Tant qu'il est question de DMA, un point de détail, mais qui a tout de même son importance, doit être mentionné. Il faut attendre un blanc vertical pour couper le canal DMA des sprites. A défaut, si un sprite était en cours d'affichage, ses données continuent d'être affichées. Cela produit un effet bien connu de "sprite qui bave verticalement".
En effet, comme expliqué précédemment lorsqu'il a été question de l'émuler au CPU, le DMA ne sert qu'à alimenter des registres SPRxPOS, SPRxCTL, SPRxDATA et SPRxDATB dans lesquels le hardware lit systématiquement les données des sprites qu'il combine à celle des bitplanes. S'il est coupé avant d'avoir écrit le dernier jeu de mots à 0 dans SPRxPOS et SPRxCTL (ce qu'il fait lorsqu'il sait que la hauteur du sprite a été parcourue, ce qui explique pourquoi ces deux mots doivent figurer à la fin des données d'un sprite), le DMA ne peut donc interrompre l'affichage du sprite. Partant, ce dernier continue d'être affiché sur tout le reste de la hauteur de l'écran avec les données figurant dans SPRxDATA et SPRxDATB au moment où le DMA a été coupé.
C'est pourquoi les programmes proposés attendent le blanc vertical (VERTB), c'est-à-dire le moment où le faisceau d'électrons a terminé de tracer l'écran, pour couper le DMA (la boucle est factorisée dans la sous-routine _waitVERTB) :
_waitVERTBLoop:
	move.w INTREQR(a5),d0
	btst #5,d0
	beq _waitVERTBLoop
	move.w #$07FF,DMACON(a5)

La cinétique : déplacer les sprites et détecter les collisions

Pour déplacer un sprite, il suffit de modifier ses coordonnées dans ses mots de contrôle à la fin d'une trame, avant que le hardware n'en refasse la lecture pour afficher la nouvelle trame. Le programme sprite.s fait ainsi se déplacer un sprite 0 de 16 pixels de hauteur en 4 couleurs, sur un décor composé d'un unique bitplane représentant un damier :
Le sprite 0, en 4 couleurs, circulant paisiblement sur un bitplane
Le hardware permet de gérer la profondeur via un système de priorités :
  • entre sprites d'abord, les priorités sont figées : le sprite 0 est toujours affiché devant le sprite 1, qui est toujours affiché devant le sprite 2, etc ;
  • entre sprites et playfields ensuite (pour rappel, le hardware peut afficher un playfield de 1 à 6 bitplanes, ou deux playfields de 1 à 3 bitplanes chacun – c'est le dual-playfield), les priorités peuvent être ajustées via le registre BPLCON2.
BPLCON2 se compose ainsi :
Bits Usage
15 à 7Inutilisé (mettre à 0)
6PF2PRI
5 à 3PF2P2 à PFR2P0
2 à 0PF1P2 à PF1P0
Dans le cas d'un seul playfield, PF2P2-0 (et non PF1P2-0, comme on l'aurait présumé) sont utilisés. Ils permettent de coder sur 3 bits le numéro du couple de sprites (et non du sprite !) derrière lequel se trouve le playfield :
Ah ! Je vous ai bien eu. Cet article traite d'Amiga et non d'Androïd !
En dual-playfield, PF1P2 à PF1P0 permettent de gérer pareillement la priorité du second playfield et des couples de sprites. Noter que ces bits traitent alors bien du playfield 1 (bitplanes 1, 3 et 5) tandis que PF2P2-0 traitent du playfield 2 (bitplanes 2, 4 et 6). Le bit PF2PRI permet de passer le playfield 2 devant le playfield 1, ce qui rajoute aux possibilités de priorisation entre sprites et playfields.
Dans les exemples déjà cités des sprites en 4 et 16 couleurs, le sprite 0 est positionné devant l'unique playfield en stockant donc $0008 dans BPLCON2.
Enfin, il faut mentionner que le hardware permet de détecter facilement une collision entre sprites, ou entre sprite et bitplanes, et cela au pixel près. Le programme spritesCollision.s montre comment il est possible de détecter les collisions entre le sprite 0 et le sprite 2, et entre chacun de ces sprites et le bitplane 1 :
Détection de collisions entre sprites et entre sprites et bitplane
La détection des collisions est assez subtile. Elle repose sur l'emploi de deux registres :
  • CLXCON permet de spécifier quels sprites et quels bitplanes impliquer ;
  • CLXDAT permet de récupérer l'état de la détection de collisions qui en résulte.
La détection de collisions implique toujours les sprites pairs. Dans CLXCON :
  • Les quatre bits ENSP7-1 permettent de spécifier que les sprites impairs doivent aussi être impliquésNoter que cela ne permet pas de détecter distinctement les collisions du sprite 0 et les collisions du sprite 1, mais de détecter les collisions de la combinaison par OR des sprites 0 et 1 : ce n'est donc vraiment utile que si ces sprites sont attachés pour afficher un sprite en 16 couleurs.
  • Les six bits ENBP6-1 permettent de spécifier si les bitplanes correspondants doivent être impliqués (si tous sont exclus, une collision avec les bitplanes est systématiquement rapportée, quelles que soient les valeurs des bits MVBP6-1).
  • Les six bits MVBP6-1 permettent de spécifier la valeur du bit dans un bitplane impliqué dans la détection de collisions qui permettra de déterminer s'il y a ou non collision avec ce bitplane.
Dans le programme spritesCollision.s, la valeur de CLXCON est $0041, ce qui revient à positionner le bit ENBP1 pour impliquer le bitplane 1 dans la détection de collisions, et le bit MVBP1 pour spécifier qu'une collision avec le bitplane 1 a lieu quand un bit à 1 est rencontré dans ce dernier.
Il suffit de lire CLXDAT (attention, car cela le remet à 0) pour récupérer à chaque trame l'état des collisions détectées entre deux entités (entre sprites, ou entre sprite et bitplane, ou entre bitplanes). Sur cette figure, on trouve à côté de chaque bit une case par sprite (0 à 7) ainsi qu'une case pour les bitplanes impairs (I) et une pour les bitplanes pairs (P). Les cases sont colorées pour indiquer la collision détectée (première entité en rouge, seconde en vert).
La composition du registre CLXDAT
Par exemple, le bit 7 sera positionné si une collision est détectée entre :
  • le sprite 4, éventuellement combiné par OR avec le sprite 5 si ENSP14 a été positionné ;
  • les bitplanes impairs, parmi ceux dont les bits ENBP ont été positionné, et pour le cas où un bit de valeur MVBP a été rencontré.
Dans le programme spritesCollision.s, les bits 9, 1 et 2 sont ainsi testés.
En permettant de spécifier sur quelle valeur de bit une collision doit être détectée entre un sprite et certains bitplanes, le hardware permet très facilement de spécifier qu'une collision ne doit être détectée que si un sprite rencontre un pixel d'un certaine couleur (éventuellement transparente) dans le décor. Puissant.
Toutefois, cette détection au pixel près n'est pas nécessairement intéressante. Un codeur de jeu vidéo sur Amiga m'a expliqué qu'il convenait mieux d'utiliser des zones de collisions sommaires, incluses dans les entités et le décor, pour moins frustrer le joueur. A cet égard, la détection de collisions du hardware reste rudimentaire. Peut-être a-t-elle été intégrée en référence à une époque où les pixels étaient si gros que cela ne posait pas de problème de gameplay.

Plus de sprites : attacher et / ou réutiliser (multiplexer) les sprites

Avec 4 couleurs, dont une transparente, la palette d'un sprite est particulièrement limitée. Sans doute, il est possible de superposer des sprites pour multiplier les couleurs, mais superposer deux sprites ne permet d'étendre la palette qu'à 8 couleurs, dont 2 transparentes. Par ailleurs, il faut se rappeler que les sprites sont couplés, et que les sprites d'un même couple partagent la même palette de 4 couleurs. Au mieux, il serait donc possible de superposer quatre sprites, étendant la palette à 16 couleurs, dont 4 transparentes. Sachant qu'il y a huit sprites, il ne serait donc possible que d'afficher deux sprites en 12 couleurs ?
Non. Comme mentionné plus tôt, les deux sprites formant un couple peuvent être combinés pour former un unique sprite, toujours de 16 pixels de large, mais cette fois affiché en 16 couleurs (les couleurs 16 à 31 de la palette de l'écran, la couleur 16 étant transparente). Les lignes du premier sprite fournissent les bits des petits bitplanes 1 et 2 du sprite, tandis que le lignes du second sprite fournissent les bits des petits bitplanes 3 et 4.
Pour combiner ainsi deux sprites d'un couple, il faut positionner le bit 7 du second mot de contrôle du second sprite du couple : c'est l'attachement. Par ailleurs, les deux sprites doivent être affichés à la même position, ce qui implique donc de les déplacer simultanément – là où ils ne se chevauchent pas, chacun est affiché dans la palette commune de 4 couleurs.
Le programme sprite16.s fait ainsi se déplacer un sprite de 16 couleurs composé des sprites 0 et 1 sur le même décor que précédemment :
Les sprites 0 et 1 combinés, en 16 couleurs, circulant paisiblement sur un bitplane
Les possibilités d'enrichir l'affichage des sprites ne s'arrêtent pas là. Si le hardware ne peut afficher un sprite qu'une fois par ligne de l'écran, il est parfaitement possible de lui demander de modifier la position d'un sprite d'une ligne à l'autre. Cela permet de démultiplier les sprites. Cette technique, aussi dite "multiplexage", est utilisée pour produire notamment des fonds étoilés, comme l'illustre le programme spritesField.s :
Réutiliser les sprites d'une ligne à l'autre pour produire un fond étoilé (multiplexage vertical)
Noter que ce programme est doublement limité, car non seulement seul le sprite 0 est réutilisé, ce qui ne permet pas de produire un effet de profondeur, mais de plus le motif du sprite n'est pas modifié d'une occurrence à l'autre, ce qui rend l'apparence très monotone. En jouant sur ces deux paramètres, il est facile de faire mieux en un instant.
Pour y parvenir, il suffit de modifier la structure des données du sprite. Les deux derniers mots à 0 doivent être remplacés par de nouveaux mots de contrôle, qui précisent les coordonnées où afficher de nouveau le sprite. Par exemple :
$2C40, $2D00	;Afficher le sprite sur 1 pixel de hauteur en ($81, $2C)
$8000, $0000	;Petits bitplanes 1 et 2 du sprite (1 pixel tout à gauche)
$2E72, $2F00	;Afficher le sprite sur 1 pixel de hauteur en ($81 + 100 = $E5, $2C + 2 = $2E)
$0008, $0000	;Petits bitplanes 1 et 2 du sprite (1 pixel tout à droite)
0, 0			;Fin du sprite
L'ordonnée de la nouvelle occurrence du sprite est contrainte. A la base, elle est nécessairement supérieure à celle de la dernière ligne affichée de la précédente occurrence du sprite. Mais il y a plus : il faut attendre une ligne à l'écran entre entre deux occurrences du sprite. En effet, à chaque ligne de l'écran, le DMA ne dispose que du temps d'alimenter le hardware avec deux mots par sprite. Ainsi, s'il lit les nouveaux mots de contrôle du sprite durant une ligne, il n'a pas le temps de lire les mots de la première ligne des petits bitplanes du sprite durant cette dernière ; c'est à la ligne suivante qu'il le pourra. En conséquence, un sprite affiché jusqu'en Y ne peut être de nouveau affiché qu'à partir de Y+2. Ce qui explique pourquoi dans le programme spritesField.s, il n'y a que 256 / 17 = 15 occurrences de 16 pixels de hauteur à l'écran.

L'astuce : cherche sprites pour plan sympa à plusieurs

S'ils sont peu nombreux et peu colorés, les sprites n'en disposent donc pas moins d'atouts pour séduire le codeur. C'est d'autant plus vrai que leur potentiel s'étend au-delà de ce qui est documenté dans l'Amiga Hardware Reference Manual. En particulier, il est possible de réutiliser des sprites horizontalement sur toute la largeur du playfield.
C'est énorme, car cela permet tout simplement de rajouter un playfield. Comme Codetapper l'a documenté sur son excellent site, l'astuce a été mise à profit avec diverses variantes dans de nombreux jeux à succès.
Le programme triplePlayfield.s en fournit une illustration en mettant en place un triple-playfield. Dans cette configuration, deux playfields sont affiché en dual-playfield, en suivant les instructions prodiguées dans l'Amiga Hardware Reference Manual. Par-dessus, un playfield de sprites est affiché en exploitant l'astuce de la réutilisation horizontale. Ce troisième playfield est composé de trois couples de sprites en 16 couleurs, réutilisés horizontalement. Si les sprites n'occupent que 32 pixels de hauteur (de quoi afficher un chiffre et un damier de couleurs), il est entendu qu'ils peuvent s'étendre sur toute la hauteur de l'écran :
Un triple-playfield, dont un playfield de sprites (multiplexage horizontal)
Le codeur de jeu vidéo sur Amiga déjà cité m'a rapporté avoir utilisé la technique pour réaliser un décor dont la topologie correspondait à un tore. En effet, le décor était répété horizontalement et verticalement, si bien que la surface de l'écran affichait effectivement une partie rectangulaire d'un décor plaqué sur un tore. Réaliser un tore avec un plan composé de sprites : je pense qu'il s'est arraché les cheveux...
Pour comprendre comment parvenir à notre bien plus modeste résultat, il faut regarder dans la Copper list. Cette dernière contient notamment une section générée ainsi :
	move.w #(DISPLAY_Y<<8)!$38!$0001,d0
	move.w #DISPLAY_DY-1,d1
_copperListSpriteY:
	move.w d0,(a0)+
	move.w #$FFFE,(a0)+
	move.w #((SPRITE_Y&$FF)<<8)!((SPRITE_X&$1FE)>>1),d2
	move.w #SPR0POS,d3
	move.w #(DISPLAY_DX>>4)-1,d4
_copperListSpriteX:
	move.w d3,(a0)+
	move.w d2,(a0)+
	addq.w #8,d3
	move.w d3,(a0)+
	move.w d2,(a0)+
	addq.w #8,d3
	cmpi.w #SPR6POS,d3
	bne _copperListSpriteNoReset
	move.w #SPR0POS,d3
_copperListSpriteNoReset:
	addi.w #16>>1,d2
	dbf d4,_copperListSpriteX
	addi.w #$0100,d0
	dbf d1,_copperListSpriteY
Le codage des instructions WAIT et MOVE du Copper a été détaillée cet article. Il suffit donc de noter que ce code génère un WAIT au début de chaque ligne de l'écran, suivi de 20 séries de MOVE qui modifient successivement les registres SPR0POS à SPR5POS.
Comme expliqué plus tôt, un registre SPRxPOS contient le premier mot de contrôle d'un sprite, donc les 8 bits de poids fort de sa position horizontale. Dans sa description du fonctionnement du DMA des sprites, l'Amiga Hardware Reference Manual précise que la position horizontale du faisceau d'électron est comparée à chaque pixel à celle qui figure dans SPRxPOS pour décider d'afficher ou non les données chargée dans SPRxDATA et SPRxDATB.
Autrement dit, si l'on change la position horizontale d'un sprite dans SPRxPOS après que ce sprite a été affiché, il est possible de provoquer de nouveau l'affichage de ce sprite sur la même ligne. C'est tout l'objet des séries de MOVE.
Prenons le cas des sprites 0 et 1, qui sont attachés pour former un sprite en 16 couleurs, et donc affichés à la même position, en haut à gauche de l'écran. Le hardware lit les données des bitplanes et des sprites à afficher par paquet de 16 pixels. Le premier WAIT attend le faisceau d'électrons à la position $38, qui correspond à la première de ces lectures qui précède le début de l'affichage (la position horizontale stockée dans DDFSTRT). Tandis que le hardware affiche les 16 pixels des sprites 0 et 1, SPR0POS et SPR1POS sont modifiés par deux MOVE qui reportent la position horizontale des sprites de 16 pixels sur la droite. Comme le Copper prend 16 pixels à exécuter ces MOVE, les nouvelles valeurs des registres sont prises en compte par le hardware à la lecture suivante qu'il fait d'un paquet de 16 pixels à afficher. Ainsi, ces sprites sont de nouveau affichés à leur nouvelle position commune !
La difficulté serait de bien synchroniser les MOVE sur la lecture des données des 16 pixels que le hardware va afficher. Toutefois, cela tombe parfaitement : nul besoin d'intercaler des MOVE inutiles, l'équivalent du NOP pour le Copper, histoire de faire une pause en cours de ligne. Noter que c'est beaucoup plus acrobatique pour d'autres effets qui consistent pareillement à duper le hardware en cours de ligne, mais à des positions précises, notamment pour le zoom hardware horizontal, dont il sera question dans un éventuel prochain article.
A ce stade, le lecteur attentif peut se poser deux questions. Pourquoi afficher un dual-playfield à l'aide de deux bitplanes par playfield (4 couleurs par playfield, dont une transparente), et non de trois (8 couleurs par playfield, dont une transparente), comme le hardware le permet ? Et pourquoi réutiliser trois couples de sprites et non quatre, ici encore comme le hardware le permet ?
C'est que l'astuce a ses limites.
Tout metal basher qui a lu son Amiga Hardware Reference Manual connaît le fameux schéma intitulé DMA time slot allocation. Ce schéma permet de visualiser l'attribution de cycles disponibles durant une ligne que le hardware trace à l'écran. Certains cycles sont réservés à des fonctions impératives, comme la lecture des données des bitplanes. Les autres sont disponibles, notamment pour le Copper qui trouve ainsi le temps d'exécuter des MOVE (accessoirement, ceux qui se demandent pourquoi un MOVE prend 8 pixels en basse résolution pour être exécuté trouveront l'explication dans le schéma).
Le problème, c'est qu'au-delà de 4 bitplanes, le hardware commence à voler des cycles auxquels le Copper aurait pu prétendre pour lire les données des bitplanes supplémentaires :
Les bitplanes 5 et 6 volent des cycles au Copper
Par conséquent, une séquence de 40 MOVE se trouve régulièrement interrompue, ce qui fait tomber la possibilité de réutiliser horizontalement autant de sprites qu'on pourrait le souhaiter. Pour disposer de tous les cycles pour réutiliser les huit sprites, il faut se limiter à 4 bitplanes, soit deux playfields de 2 bitplanes chacun.
Par ailleurs, dans le programme triplePlayfield.s, les playfields défilent horizontalement en exploitant les possibilités du hardware via le registre BPLCON1. Or cet effet repose sur une lecture anticipée des données des bitplanes (16 pixels avant le début effectif de leur affichage), ce qui vient ici encore voler des cycles, mais cette fois en interdisant de lire via DMA les données du sprite 7 :
Le scrolling des playfields volent les cycles du DMA pour les sprites 6 et 7
De ce fait, le couple de sprites 6 et 7 ne peut être utilisé.

L'AGA : des sprites un peu plus mieux

Alors que les codeurs avaient particulièrement apprécié la qualité du Amiga Hardware Reference Manual documentant l'Original Chip Set (OCS) dans ses moindres détails, Commodore fit le choix de ne pas documenter l'Advanced Graphics Architecture (AGA ici, AA chez les ricains). Cette politique visait à assurer la compatibilité des logiciels dans la perspective d'une révolution du chipset qui, comme on le sait, ne survint jamais.
La décision fut conspuée par les codeurs. Dans leur présentation de l'Amiga 1200 publiée dans Grapevine #14, Ringo Star / Classic et Animal & Goofy / Silents ne firent qu'exprimer le sentiment général... non sans faire preuve de la toujours distrayante vantardise des membres s'estimant en vue de la scène !
Ringo Star / Classic et Animal & Goofy / Silents, pas contents !
Or ce numéro de Grapevine contient un autre article, moins plastronnant et plus constructif, publié par votre serviteur et son acolyte Junkie / PMC (ainsi qu'un certain Spencer Shanson dont le rôle dans cette affaire m'échappe désormais...).
L'excellent Junkie / PMC ayant eu l'idée de désassembler la Copper list du Workbench de l'Amiga 1200, nous avions passé un bon moment à faire la rétro-ingénierie du hardware en testant bit à bit ses registres. Tout cela couché sur le papier, il en avait résulté une documentation officieuse expliquant comment tirer parti des principales fonctionnalités de l'AGA par metal bashing comme on l'aime. Cette documentation fut ensuite complétée et corrigée par de gentils contributeurs, notamment Randy / Comax dont la version reste visiblement à ce jour la plus aboutie.
En ce qui concerne les sprites, les capacités de l'AGA surpassent sans conteste celles de l'OCS, mais c'est plus d'une évolution que d'une révolution dont il s'agit. En effet, il n'est toujours possible d'afficher que 8 sprites en 4 couleurs (palettes différentes pour les sprites pairs et impairs), ou 4 sprites en 16 couleurs (même palette pour tous les sprites). Toutefois :
  • la largeur d'un sprite peut être de 16, 32 ou 64 pixels ;
  • les groupes de sprites pairs et impairs peuvent partager la même palette de 16 couleurs ou chaque groupe peut disposer de la sienne (lorsque les sprites sont attachés, c'est la palette des sprites impairs qui est utilisée) ;
  • la résolution des sprites peut être basse, haute ou super-haute ;
  • la position des sprites peut être ajustée à l'équivalent d'un pixel en basse, haute ou super-haute résolution ;
  • chaque ligne d’un sprite peut être doublée sans que cela nécessite de doubler la ligne dans les données de ses petits bitplanes ;
  • les sprites peuvent être affichés sur les bords de l'écran, c'est-à-dire au-delà des bitplanes, dans la zone normalement tracée en couleur 0.
Il ne sera pas question de rentrer plus dans les détails ici, car ce serait grosso modo redire tout ce qui a déjà été dit sur les sprites. Pour en savoir plus, le lecteur peut toujours se reporter au programme spritesAGA.s. Ce programme exploite ces fonctionnalités en affichant quatre sprites de 64 pixels de large en 16 couleurs sur un décor en 256 couleurs, c'est-à-dire à base de 8 bitplanes, y compris dans les bords de l'écran :
Des sprites un peu plus mieux en AGA

Pour en finir avec les sprites...

Etant pris en charge par le hardware, les sprites présentent plusieurs avantages pour le codeur. En effet, ce dernier n'a pas à gérer la préservation et la restauration du décor sur lequel ils sont affichés (le recover), pas plus que le découpage pouvant aller jusqu'à l'élimination quand ils débordent ou sortent du playfied (le clipping).
Toutefois, les sprites sont comme les hobbits : pratiques et colorés, mais petits et peu nombreux. C'est pourquoi les codeurs utilisent aussi, voire alternativement, des BOBs. L'acronyme correspond à Blitter OBject, c'est-à-dire des bitmaps dessinés au Blitter. Parce que les BOBs sont si souvent utilisés, le second article de cette série consacrée à l'affichage de bitmaps sur Amiga en exposera les détails.
Afficher des sprites et des BOBs sur Amiga OCS et AGA (1/2)