Cet article est le second, et donc le dernier, d'une série consacrée au développement d'une cracktro sur Amiga. Dans le premier article, nous avons vu comment mettre en place un environnement de développement en assembleur 68000 dans le contexte d'un émulateur Amiga, et nous sommes rentrés dans une présentation d'un des deux coprocesseurs graphique, le Blitter, exemple à l'appui. Ici, nous présenterons pareillement le Copper et nous conclurons sur l'intérêt de revisiter ce passé.
Pour rappel, vous pouvez visualiser un enregistrement de la cracktro sur YouTube ou un parfait portage en HTML5 sur Flashtro. Quant au code (à peine un gros millier d'instructions en assembleur 68000), vous pouvez le télécharger en cliquant ici.
Les bases de l'affichage
Comme chacun sait, une image affichée à l'écran est composée de pixels, chaque pixel étant codé en mémoire par une valeur qui permet de déterminer la couleur dans laquelle il doit être affiché. De nos jours, cette valeur est généralement un quad, les composantes rouge, verte et bleue de la couleur du pixel étant codées chacune sur un octet (le quatrième octet étant réservé au degré de transparence, ou alpha). Sur Amiga, la solution est très différente.
Exception faite du mode Hold-And-Modify (HAM) exceptionnellement utilisé, l'affichage sur Amiga fonctionne sur le principe suivant. Pour afficher en N couleurs, N étant une puissance de 2, il faut N plans de bits, ou bitplanes, dont les dimensions correspondent à celles de la surface affichée - par exemple, 320x256 pixels. Le premier bitplane fournit les bits 0 des pixels, le deuxième fournit les bits 1 des pixels, etc.
L'ensemble des bits d'un pixel issus des bitplanes aux coordonnées du pixel en question constitue une valeur. Cette valeur correspond à l'index de la couleur dans laquelle le pixel doit être affiché. Cette couleur est extraite d'une palette de couleurs correspondant à la suite des registres COLOR00 à COLOR31. Enfin, cette couleur est codée sur un mot où chacune des composantes rouge, verte et bleue est codée sur quatre bits, ce qui donne 4 096 possibilités. Au passage, il convient de noter que les N bitplanes sont par convention étrangement numérotés de 1 à N.
L'Amiga peut afficher en basse ou haute résolution, ce qui joue sur le nombre de couleurs :
- En basse résolution (ie : 320x256 en PAL), l'Amiga peut afficher au plus 5 bitplanes, donc 32 couleurs. Il existe un mode particulier, l'Extra Half-Brite (EHB) où l'Amiga peut afficher 6 bitplanes, mais les 32 couleurs supplémentaires sont alors des variantes des 32 premières dont la luminosité est automatiquement divisée par 2.
- En haute résolution (ie : 640x256 en PAL), l'Amiga peut afficher au plus 4 bitplanes, donc en 16 couleurs.
Dans les deux cas, le nombre de lignes peut être doublé en activant un mode entrelacé. C'est au prix d'un scintillement, car l'entrelacé consiste à afficher alternativement les lignes paires et impaires en misant sur un effet de rémanence visuelle.
Ces possibilités ont été étendues au fil des évolutions du hardware, mais on se limitera ici à évoquer l'Amiga 500, donc l'Original Chip Set (OCS) et non les suivants - Ehanced Chip Set (ECS) pour l'Amiga 500+ et Advanced Graphics Architecture (AGA) pour l'Amiga 1200.
Parler de basse ou haute résolution en évoquant un nombre de pixels affichés horizontalement est un abus de langage. En fait, il faut entendre que le faisceau d'électrons met un certain temps à balayer une ligne de l'écran, et qu'il est possible de lui demander de tracer plus ou moins longtemps, donc sur une certaine longueur, un pixel : 140 nanosecondes en basse résolution, 70 en haute résolution.
C'est pourquoi indépendamment de la résolution, il faut distinguer la surface d'affichage et les données affichées. La surface correspond à la partie de la région balayée par le faisceau d'électrons effectivement utilisée. Elle doit être spécifiée via les registres DIWSTART et DIWSTOP. D'autres registres, DDFSTART et DDFSTOP, permettent de spécifier les positions horizontales du faisceau d'électrons entre lesquelles les données doivent être lues et affichées au rythme imposé par la résolution adoptée. Les données doivent être lues un peu avant leur affichage pour laisser le temps au hardware de jouer sur le faisceau d'électrons.
Jouer la "playlist" vidéo
Le Copper est un des coprocesseurs de l'Amiga, essentiellement utilisé pour contrôler l'affichage en écrivant dans des registres tels que ceux déjà cités pour spécifier des paramètres de ce dernier.
Avant d'aller plus loin, il faut clarifier deux points :
- Tout d'abord, le recours au Copper pour contrôler l'affichage est facultatif. Il serait parfaitement possible d'écrire dans les registres avec le CPU pour parvenir au même résultat. Toutefois, il faudrait synchroniser ces écritures avec la position du faisceau d'électrons, et ne pas manquer de rafraîchir les registres dont le contenu est modifié lors d'une trame - par exemple BPL1PTH et BPL1PTL, registres qui contiennent l'adresse du bitplane 1, sont incrémentés tandis que ce bitplane est affiché. Dans ces conditions, pourquoi ne pas utiliser le Copper ?
- Ensuite, il a été dit, le Copper est essentiellement utilisé pour contrôler l'affichage. C'est que le Copper peut écrire dans d'autres registres qui permettent de contrôler d'autres fonctionnalités du hardware - pas tous les registres disponibles, mais bon nombre d'entre eux. Par exemple, le Copper peut fort bien écrire dans les registres qui contrôlent le Blitter pour paramétrer et déclencher la copie d'un bloc en mémoire ou un tracé de ligne.
Dans le précédent article, nous avons vu que le Blitter se programme en attaquant des registres. Pour sa part, le Copper se programme aussi en attaquant des registres, mais surtout en lui fournissant une liste composée d'instructions : la Copper list, qui est exécutée automatiquement à chaque trame.
Dans la Copper list, une instruction est fournie sous la forme de deux mots. Autrement dit, le Copper se programme directement en opcodes, généralement écrits en hexadécimal. Le Copper détecte de quelle instruction il s'agit en se basant sur la combinaison des deux bits 0 des mots d'une instruction.
Le Copper comprend simplement trois instructions :
- WAIT indique au Copper qu'il doit attendre que le faisceau d'électron atteigne ou dépasse certaines coordonnées à l'écran. Si l'ordonnée est simplement le numéro de la ligne indépendante de la résolution, l'abscisse est le numéro d'une série de pixels dont la longueur dépend de cette dernière. Ainsi, en 320 x 256, l'abscisse peut être précisée tous les quatre pixels. Ordonnée et abscisse peuvent être masquées, mais cette fonctionnalité est exceptionnellement utilisée. Généralement, une instruction WAIT prend donc la forme d'un premier mot ((y & $FF) << 8) | ((x & $FE) << 1) | $0001 et d'un second mot $FFFE, le masquage étant ainsi inhibé.
- MOVE indique au Copper qu'il doit écrire un mot dans un des registres dont l'offset (nécessairement pair) est fourni en présumant qu'il va être ajouté à l'adresse $DFF000 pour localiser ce registre en mémoire. Par exemple, $01820FF0 permet d'écrire $0FF0 dans $DFF182 (registre COLOR01), ce qui revient à passer la couleur 1 de la palette à jaune (dans ce registre, la couleur est codée sur les 12 premiers bits, 4 bits par composante. Le Copper ne peut pas écrire dans tous les registres $DFFxxx, et dans certains il ne peut écrire que sous condition de positionnement d'un bit particulier soit positionné - ce bit permet donc de sécuriser l'exécution de la Copper list en prévenant des écritures par débordement.
- SKIP indique au Copper qu'il doit sauter l'instruction qui suit si jamais le faisceau d'électrons atteint ou dépasse certaines coordonnées à l'écran. Ces coordonnées sont spécifiées comme pour l'instruction WAIT.
Un WAIT $FFFFFFFE indique au Copper qu'il a atteint la fin de la Copper list.
Autant l’intérêt de MOVE et de WAIT est évident, autant celui de SKIP ne l’est pas. SKIP est utile dans le contexte d’usages (très) avancés, tout particulièrement :
- modifier les registres COPxLCH, COPxLCL et COPJMPx pour sauter de Copper list en Copper list et ainsi réaliser des boucles ;
- déclencher et attendre le Blitter – un bit d’un WAIT dont l’existence n’a pas été mentionnée plus tôt le permet.
La Copper list doit être écrite en mémoire à une adresse qu'il faut fournir au Copper via ses registres COP1LCH et COP1LCL. Une fois l'adresse écrite, une écriture dans le registre COPJMP1 permet de demander au Copper de désormais traiter en boucle la Copper list désignée - tout comme BLTSIZE, COPJMP1 est un strobe. Il ne reste plus alors qu'à activer l'accès à la mémoire du Copper pour visualiser le résultat à l'écran, ce qui s'effectue via certains bits du registre DMACON.
Dans la cracktro, la Copper list permet d'organiser l'affichage de la manière suivante, la transition entre la première et la seconde partie s'effectuant à l'aide d'un WAIT :
- une première partie de l'écran en basse résolution de 320x160 pixels sur 5 bitplanes (0 et 1 pour un des cubes, 2 et 3 pour l'autre, et 4 pour le damier) ;
- une seconde partie de l'écran en haute résolution et entrelacé de 640x92 pixels sur 1 bitplane.
Par ailleurs, la Copper list est modifié à chaque trame pour modifier les valeurs stockées dans les registres des couleurs des cubes en fonction des faces visibles. En fait, il y a deux Copper list identiques, l'une activant l'autre quand elle se termine en écrivant dans COP1LCH et COP1LCL l'adresse de son homologue - c'est une condition du fonctionnement de l'affichage en entrelacé.
Il serait trop long de restituer ici le code de la Copper list de la cracktro. Réécrite à l'occasion de la rédaction de cet article en s'en inspirant, voici une Copper list basique. Elle commande l'affichage d'un écran en 320x256 en deux couleurs (un unique bitplane) en changeant la couleur de fond à mi-hauteur :
Les variables copperlist et bitplane contiennent les adresses de zones de mémoire réservées par allocation système pour stocker les données de la Copper list et du bitplane.
DISPLAY_DX=320 DISPLAY_DY=256 DISPLAY_X=$81 DISPLAY_Y=$2C DISPLAY_DEPTH=1 movea.l copperlist,a0 movea.l copperlist,a0 move.w #$008E,(a0)+ ;DIWSTRT move.w #(DISPLAY_Y<<8)!DISPLAY_X,(a0)+ move.w #$0090,(a0)+ ;DIWSTOP move.w #((DISPLAY_Y+DISPLAY_DY-256)<<8)!(DISPLAY_X+DISPLAY_DX-256),(a0)+ move.w #$0100,(a0)+ ;BPLCON0 move.w #(DISPLAY_DEPTH<<12)!$0200,(a0)+ move.w #$0102,(a0)+ ;BPLCON1 move.w #$0000,(a0)+ move.w #$0104,(a0)+ ;BPLCON2 move.w #$0000,(a0)+ move.w #$0092,(a0)+ ;DDFSTRT move.w #((DISPLAY_X-17)>>1)&$00FC,(a0)+ move.w #$0094,(a0)+ ;DDFSTOP move.w #((DISPLAY_X-17+(((DISPLAY_DX>>4)-1)<<4))>>1)&$00FC,(a0)+ move.w #$0108,(a0)+ ;BPL1MOD uniquement car un seul bitplane move.w #0,(a0)+ ;Adresse du bitplane move.w #$00E0,(a0)+ ;BPL1PTH move.l bitplane,d0 swap d0 move.w d0,(a0)+ move.w #$00E2,(a0)+ ;BPL1PTL swap d0 move.w d0,(a0)+ ;Couleur de fond à rouge move.w #$0180,(a0)+ ;COLOR00 move.w #$0F00,(a0)+ ;Attendre le milieu de l'écran (128 lignes sous $32) move.w #(((DISPLAY_Y+(DISPLAY_DY>>1))&$00FF)<<8)!$0001,(a0)+ move.w #$FF00,(a0)+ ;Couleur de fond à bleu move.w #$0180,(a0)+ ;COLOR00 move.w #$000F,(a0)+ ;Comptabilité ECS avec l'AGA move.l #$01FC0000,(a0)+ ;Fin de la Copperlist move.l #$FFFFFFFE,(a0) ;Activer la Copper list move.w #$7FFF,$DFF096 ;DMACON move.l copperlist,$DFF080 ;COP1LCH et COP1LCL clr.w $DFF088 ;COPJMP1 move.w #$8380,$DFF096 ;DMACON
Au passage, un effet très couru consiste à enchaîner les MOVE pour changer la couleur de fond sur le nombre de pixels que le faisceau d'électrons parcourt le temps que cette instruction soit exécutée, soit le temps d'afficher 8 pixels en basse résolution. En répétant une séquence de couleurs formant un dégradé cyclique sur une ligne, et en répétant cette opération sur plusieurs lignes en commençant à chaque ligne dans la séquence à un indice qui varie de ligne en ligne selon une fonction telle qu'un sinus, on produit un effet "plasma" qui peut être animé, comme dans la très belle partie Plasmaworld de la Megademo du groupe Humanoids :
Dans la cracktro, la Copper list est notamment modifiée à chaque trame pour étendre artificiellement la palette des couleurs utilisées pour afficher les faces d'un cube. En effet, comme expliqué dans l'article précédent, chaque cube est affiché sur deux bitplanes, ce qui permet d'afficher en quatre couleurs.
Or les faces opposées d'un cube sont mutuellement exclusives. Par exemple, quand la face avant est visible, la face arrière est nécessairement cachée - pour déterminer si une face est visible, l'orientation de sa normale est testée. Dès lors, l'astuce consiste à afficher des faces opposées de sorte que leurs pixels référencent la même couleur dans la palette, couleur qui leur est réservée. Quand une face est visible, sa couleur est modifiée en écrivant dans la Copper list pour écraser la valeur que cette dernière écrit dans le registre correspondant. Par exemple, sans que cela corresponde nécessairement à ce qui se retrouve dans la cracktro :
Face | Bitplane 2 | Bitplane 1 | Registre | Valeur |
Haut | Non | Oui | %01 = COLOR01 | $0F00 |
Bas | Non | Oui | %01 = COLOR01 | $0F00 |
Gauche | Oui | Non | %10 = COLOR02 | $000F |
Droite | Oui | Non | %10 = COLOR02 | $00FF |
Avant | Oui | Oui | %11 = COLOR03 | $0FF0 |
Arrière | Oui | Oui | %11 = COLOR03 | $0F0F |
Chaque face étant affichée dans une couleur qui lui est propre, le cube semble affiché en sept couleurs alors qu'il n'est affiché que sur deux bitplanes.
Sprites, dual-playfield et scrolling... hardware !
Le hardware de l'Amiga ne permet pas que d'afficher des bitplanes. Il permet aussi d'afficher des sprites, de découpler l'affichage des bitplanes sur deux plans - le dual-playfield -, de faire défiler un plan des bitplanes : autant de fonctionnalités qu'il encore une fois possible de contrôler au CPU ou au Copper en écrivant dans certains registres.
La cracktro n'utilise aucune de ces fonctionnalités, mais il serait malheureux de ne pas profiter de l'occasion pour les évoquer car elles permettent de réaliser des effets graphiques qu'on retrouve dans nombre de démos et autres cracktros.
Les sprites. Chacun sait ce qu'est un sprite : une image qui doit être affichée par-dessus un décor dans lequel elle se déplace en évitant de recouvrir les pixels qui, dans le sprite, sont censés être transparents - une couleur ou un masque permet de les identifier. Normalement, l'affichage des sprites est effectué au processeur, ce qui implique d'écrire le code requis pour non seulement afficher mais aussi effacer le sprite en restaurant la partie du décor qu'il recouvrait dans sa position antérieure.
Sur Amiga, ces sprites software sont souvent affichés au Blitter car ce dernier permet de combiner logiquement lors d'une seule copie les pixels du sprite, de son masque et du décor - raison pour laquelle ces sprites sont désignés comme des "bobs", pour "Blitter objects". Aussi le terme de sprite est-il réservé pour désigner les sprites hardware du Copper.
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 (0 et 1 utilisent les couleurs 16 à 19, 2 et 3 utilisent les couleurs 20 à 23, etc.), et ils peuvent de plus être combinés. Deux sprites combinés forment un sprite toujours de 16 pixels de large et d'hauteur illimitée, mais en 16 couleurs dont une couleur transparente. Enfin, un système de priorités permet de gérer finement quel sprite est affiché devant ou derrière quel playfield (cf. ci-après).
Un sprite se contrôle très simplement. En effet, ses données se présentent sous la forme d'une liste de mots. Les deux premiers permettent de spécifier l'abscisse, l'ordonnée, et la hauteur du sprite. Suivent deux mots par ligne du sprite : le premier donne les bits de poids faible et le second les bits de poids fort permettant de déterminer l'indice de la couleur des pixels correspondant du sprite. Un couple de mots nuls marque la fin des données du sprite. Par exemple, un sprite formant un petit damier cases de 4x4 pixels dans ses 4 différentes couleurs affiché en (23, 23) dans un repère classique commençant en ($81, $2C) :
sprite: dc.w $444C, $5401 ;Coordonnées (codage un peu pénible) REPT 4 dc.w $0F0F, $00FF ;Première ligne de cases ENDR REPT 4 dc.w $F0F0, $0FF0 ;Seconde ligne de cases ENDR REPT 4 dc.w $0F0F, $FF00 ;Troisième ligne de cases ENDR REPT 4 dc.w $F0F0, $F00F ;Quatrième ligne de case ENDR dc.w $0000, $0000 ;Fin
D'ingénieux codeurs maîtrisant parfaitement le hardware ont élaboré des techniques très élaborées pour enrichir l'affichage en jouant avec le Copper, notamment avec les sprites. Le site Codetapper permet de constater tout ce que l'analyse de la Copper list de différents jeux à succès révèle... Passionnant !
Le dual playfield. En positionnant simplement un bit dans un registre, les 6 bitplanes qu'il est possible d'utiliser peuvent être regroupés : les pairs, d'une part, les impairs, d'autre part. Chaque ensemble constitue alors un playfield, c'est-à-dire un décor à part entière doté d'une palette de 8 couleurs, dont une couleur transparente.
Le dual playfield permet d'afficher les deux décors avec effet de transparence, l'un laissant entrevoir l'autre dans ses zones de pixels de couleur transparente, sans avoir à faire intervenir le CPU. Comme par ailleurs, le scrolling de chaque playfield peut être géré indépendamment de l'autre tant verticalement qu'horizontalement (cf. ci-après), le dual playfield permet de réaliser un effet de parallaxe où un décor se déplace derrière l'autre à une vitesse différente, comme s'il se trouvait à une certaine distance de l'autre le long d'un axe qui, partant de l'observateur, s'éloigne vers le fond de l'écran.
Le scrolling. Pour afficher un bitplane, il faut en fournir l'adresse au hardware dans une paire de registres : BPL1PTH et BPL1PTL pour le bitplane 1, BPL2PTH et BPL2PTL pour le bitplane 2, etc. Or, que ce soit pour le groupe des bitplanes pairs, d'une part, ou celui des bitplanes impairs, d'autre part, il est possible de contrôler deux paramètres du hardware lorsqu'il lit les données d'un groupe de bitplanes pour afficher une ligne de pixels :
- retarder de 0 à 15 pixels l'affichage d'une ligne ;
- sauter un certain nombre d'octets (le modulo) à la fin d'une ligne.
En combinant ces deux paramètres, il est possible de produire un scrolling non seulement horizontal, mais aussi vertical, un effet ici encore totalement pris en charge par le hardware.
C'est ce mécanisme qui peut être utilisé pour produire l'effet de parallaxe dont il a été question plus tôt.
Pour être complet dans cet inventaire des fonctionnalités vidéo du hardware, il faut mentionner la détection de collisions à la précision du pixel. Toutefois, c'est une fonctionnalité plus utile aux jeux qu'aux démos et autres cracktros. Quoi que... ?
Que retenir de tout cela pour aujourd'hui ?
Comme expliqué dans l'introduction du premier article de cette série, le propos de cette présentation illustrée de la programmation des deux coprocesseurs graphiques de l'Amiga en assembleur n'était certainement pas d'inciter à programmer sur cette machine de nos jours, même si cela peut être des plus instructifs.
Le propos était plutôt d'enrichir la culture générale de ceux qui n'ont jamais eu l'occasion de s'adonner à une telle programmation en leur montrant brièvement en quoi elle pouvait consister. Au passage, il s'agissait aussi montrer à tous comment l'affichage d'une image à l'écran peut, à bas niveau, fonctionner - un mécanisme qu'il est d'autant plus essentiel de connaître que son principe n'a, somme toute, guère évolué.
Rappelons qu'il subsiste un petit monde de demomakers qui produisent des exercices de style fascinants mêlant graphismes, musiques et code, désormais bien plus sophistiqués que la simple cracktro présentée - qui, même si elle n'était pas mal, était loin d'être un chef-d'œuvre de l'époque dans son genre. On ne saurait trop conseiller aux jeunes de rejoindre cette "scène". Même s'ils ne feront pas de la programmation de routines graphiques leur métier, ils acquerront une connaissance du hard et du soft qui leur servira dans bien d'autres circonstances par la suite.
Ah ! Peut-être un conseil pour tous pour terminer... Comme déjà mentionné, c'est l'Amiga Hardware Reference Manual rédigé par les ingénieurs de Commodore qui a servi de référence pour élaborer cette série d'articles. Autrement dit, l'information à la source. Or c'est toujours à la source qu'il faut s'alimenter pour découvrir une technologie : même si elle paraît indigeste parce que très technique, privilégiez la documentation de référence sur la documentation dérivée. Par exemple, si vous souhaitez faire du JavaScript, commencez par lire la spécification ECMAScript. A défaut, vous risquez de penser que JavaScript est un langage orienté objet à base de classes et, commettant cette erreur, vous passerez à côté d'aspects fondamentaux du langage.