Le choc de l'arrivée de la Super Nintendo fin 1991 pour les amateurs de micros 16 bits, ce fut le Mode 7. La console était capable d'appliquer une rotation et un redimensionnement à un bitmap de grande taille dans la trame, et il était possible de trafiquer pour produire un effet de perspective.
L'Amiga 500 était doublement handicapé pour parvenir à produire de tels effets : pas de circuit spécialisé pour cela, et une organisation des données affichées sous forme de bitplanes exigeant, pour y parvenir, de nombreuses opérations au CPU et/ou au Blitter. Toutefois, cela n'empêcha pas certains de produire des effets du genre, par exemple le zoom au démarrage de la démo World of Commodore 92 de Sanity :
Parmi tous ces effets de zoom avec ou sans perspective, la plupart se sont appuyés sur le zoom vertical hardware, découvert dès les débuts de l'Amiga, et certains sur le zoom horizontal hardware, qui l'a été bien plus tard.
Comment procéder à l'un et l'autre de ces zooms avec le hardware ? En particulier, quel est ce fameux "$102 trick" régulièrement évoqué dans les forums de demomakers, souvent succinctement, et parfois abusivement ? Et dans quelle mesure est-il possible de zoomer ainsi ? Tout cela et plus encore dans ce qui suit.
Mise à jour du 11/09/2018 (matin) : Correction de la figure représentant le scénario du zoom horizontal hardware (une étape de trop!) et de la "magic table" (bogue dans le programme HTML5 générateur!).
Mise à jour du 11/09/2018 (soir) : Ajout d'un paragraphe et d'une figure pour expliquer pourquoi le zoom horizontal hardware limite à 4 le nombre de bitplanes.
Mise à jour du 12/09/2018 (matin) : Modification de la fin de la section sur le passage de la dissimulation à la suppression, pour expliquer pourquoi et comment il faut optimiser.
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 :
- zoom0.s pour dissimuler par zoom vertical hardware des lignes avant, au milieu et en bas d'une image et recentrer cette dernière verticalement à l'écran ;
- zoom1.s pour identifier quand utiliser le Copper pour modifier la valeur de BPLCON1 afin de dissimuler un certain nombre de colonnes formées des derniers pixels d'un groupe de 16 pixels ;
- zoom2.s pour visualiser le résultat produit par le zoom horizontal hardware reposant sur la technique précédente généralisée pour dissimuler de 1 à 15 colonnes de pixels d'une image ;
- zoom3.s pour tester un zoom reposant sur la technique précédente pour réduire une image de 306 à 15 pixels de largeur ;
- zoom4.s pour tester un zoom combinant zoom horizontal hardware et zoom vertical hardware pour réduire une image de 306 x 256 pixels à 15 x 15 pixels.
NB : Cet article se lit mieux en écoutant l'excellent module Helmet for sale composé par Jason / Kefrens pour R.A.W. #2, mais c'est affaire de goût personnel...
Le zoom vertical hardware, un usage banal des modulos
Comme expliqué ici, le hardware affiche une image composée de bitplanes dont les adresses lui sont fournies via les couples de registres BPLxPTH / BPLxPTL. Une fois qu'il a lu et affiché les données d'une ligne, le hardware rajoute l'équivalent en octets à ces adresses - 40 octets pour un écran de 320 pixels de large -, puis il y rajoute un modulo. Ce modulo est stocké dans BPL1MOD pour les bitplanes impairs (1, 3, etc.), et BPL2MOD pour les bitplanes pairs (2, 4, etc.).
Sachant que le hardware lit donc dans des registres que le Copper permet de modifier (au moins) à chaque ligne de l'écran, on conçoit facilement comment tirer profit de ce fonctionnement. Prenons l'exemple d'une image de 320 x 256 pixels en 16 couleurs, donc 4 bitplanes.
Il est possible de demander au Copper d'attendre le début d'une ligne et d'écrire dans les registres BPLxPTH/L pour modifier l'adresse de la ligne qui sera affichée. Cette adresse sera déterminée en fonction du zoom, pour sauter ou répéter des lignes, ou simplement passer à la ligne suivante. La Copper list contient alors notamment un bloc suivant par ligne N, ici présenté en pseudo-code pour en faciliter la lecture :
WAIT <début de ligne N> MOVE <valeur>, BLTP1PTH MOVE <valeur>, BLTP1PTL MOVE <valeur>, BLTP2PTH MOVE <valeur>, BLTP2PTL MOVE <valeur>, BLTP3PTH MOVE <valeur>, BLTP3PTL MOVE <valeur>, BLTP4PTH MOVE <valeur>, BLTP4PTL
Qui est certain de l'alignement des données en mémoire peut se dispenser d'écrire dans BPLxPTH sachant que sa valeur ne changera pas quel que soit l'offset rajouté pour progresser dans les bitplanes, mais peu importe : c'est le principe qu'on illustre ici.
Il est aussi possible de simplement écrire dans BPL1MOD et BPL2MOD. Dans ce cas, c'est l'adresse de la ligne suivante, et non l'adresse de la ligne courante, qui sera impactée, le hardware utilisant les modulos en fin de ligne. La Copper list contient alors notamment un bloc suivant par ligne N, ici encore présenté en pseudo-code pour en faciliter la lecture :
WAIT <début de ligne N-1> MOVE <valeur>, BPL1MOD MOVE <valeur>, BPL2MOD
Utiliser BPLxMOD affecte tous les bitplanes impairs et/ou tous les bitplanes pairs, sans discrimination. Toutefois ce n'est pas gênant, car le zoom à réaliser est généralement le seul effet à produire dans les bitplanes. Par ailleurs, utiliser ces registres est plus économique qu'utiliser BPLxPTH/L.
En effet, il ne faut pas oublier que le facteur de zoom variant d'une trame à l'autre, la Copper list devra être modifiée au CPU ou au Blitter pour mettre à jour les valeurs que le Copper écrit dans les registres utilisés. Or le calcul est vite fait :
- avec BPLxPTH/L, il faut modifier les valeurs de deux MOVE par bitplanes, soit 8 valeurs au total (éventuellement 4 en jouant sur l'alignement en mémoire) ;
- avec BPLxMOD, il faut modifier les valeurs de deux MOVE tout court, soit 2 valeurs au total.
Toutefois, il ne suffit pas de dissimuler des lignes pour produire un zoom : à toute étape, il faut que l'image reste centrée à l'écran. Or modifier les modulos entraîne le tassement de l'image vers le haut de l'écran. Pour compenser il faut repousser vers le bas l'image de la moitié du nombre de lignes dissimulées. C'est possible grâce au moins à deux solutions :
- modifier la position verticale de départ de l'affichage dans le registre DIWSTRT, et la hauteur de cet affichage dans le registre DIWSTOP ;
- modifier la position verticale d'un WAIT du Copper, position à partir de laquelle des MOVE modifient les registres BPLxPTH/L pour pointer sur les adresses de départ des bitplanes de l'image zoomée.
Dans l'un et l'autre cas, il est inutile de modifier la position verticale des WAIT qui indiquent au Copper à quelle ligne il doit attendre avant d'exécuter le MOVE qui, en écrivant une valeur dans BPLxMOD, entraîne ou non la dissimulation d'une ou plusieurs lignes. En effet, à chaque étape du zoom, il suffit d'actualiser directement dans le code de la Copper list les valeurs que ces MOVE écrivent dans BPLxMOD, en tenant compte de la nouvelle position verticale de l'image pour sélectionner ces MOVE. Bref, s'il faut réduire la hauteur d'une image de 256 à 0 pixels en N étapes, il suffit de construire une Copper list qui contient toujours 256 WAIT suivis de MOVE sur BPLxMOD, et de modifier à chaque étape les valeurs que ces MOVE écrivent dans BPLxMOD pour animer le zoom.
Dans le programme final zoom4.s, c'est la première solution qui a été adoptée par simplicité. Il faut noter que cela implique de modifier BPLxPTH/L pour parvenir à dissimuler une ou plusieurs des premières lignes. En effet, le modulo est une valeur que le hardware ajoute à l'adresse de la ligne qui vient d'être affichée. Or par définition, la première ligne ne vient après aucune autre ligne. Toutefois, c'est le seul cas où ces registres sont modifiés pour zoomer.
Par ailleurs, il faut noter que le faisceau d'électrons ne trace donc rien au-dessus ni en-dessous de l'image zoomée. Dans ces conditions, il est impossible d'afficher des bandeaux encadrant l'image zoomée. Pour ce faire, il faut opter pour la seconde solution.
La figure suivante récapitule ce qui se passe dans le cas où les lignes 0, 1, 2 et 5 de l'image doivent être dissimulées :
- Comme il s'agit de dissimuler quatre lignes, DIWSTART est modifié pour que l'affichage démarre deux lignes plus bas, assurant ainsi que l'image reste verticalement centrée à l'écran.
- En conséquence, DIWSTOP est aussi réduit pour que le hardware n'affiche pas du garbage après avoir affiché la 256ème ligne de l'image.
- Pour dissimuler les trois premières lignes, impossible d'utiliser BPLxMOD. C'est donc BPLxPTH/L qui est modifié avant le début de l'affichage de la ligne 3.
- Pour dissimuler la ligne 5, BPLxMOD est passé à l'équivalent d'une ligne en octets, soit 40 octets, avant la fin de l'affichage de la ligne 4.
Le programme zoom0.s constitue un exemple de base où les 16 premières lignes, les 16 lignes médianes et les 16 dernières lignes d'une image sont ainsi dissimulées - pour la beauté du geste, c'est l'image Dragon Sun de Cougar / Sanity qui est utilisée :
A ce stade, il reste un point essentiel à régler : comment identifier les lignes à dissimuler à une étape du zoom donnée ? Cette question, qui se pose presque dans les mêmes termes pour le choix des colonnes à dissimuler, sera abordée plus loin.
Le zoom horizontal hardware, un détournement surprenant du scrolling
L'histoire dira à qui revient le mérite d'avoir trouvé l'astuce (on évoque ici le génial Chaos / Sanity). Comme chacun sait, il est possible de demander au hardware de retarder l'affichage de l'image à l'écran d'un nombre de pixels allant de 0 à 15. La valeur de ce retard doit être spécifiée dans le registre BPLCON1, qui comporte 4 bits (PF1H3-0) pour le retard des bitplanes impairs (1, 3, etc.) et 4 bits (PF2H3-0) pour le retard des bitplanes impairs (2, 4, etc.). C'est le scrolling hardware.
Ainsi, une valeur de $005F dans BPLCON1 va retarder l'affichage des bitplanes pairs de 5 pixels et celui des bitplanes impairs de 15 pixels. A moins d'utiliser le dual-playfield ou de chercher à manipuler distinctement les bitplanes pairs et impairs, les retards spécifiés pour ces bitplanes seront identiques. Il s'agira de produire un scrolling horizontal en jouant sur BPLCON1 et sur les couples BPLxPTH / BPLxPTL - on ne s'étendra pas sur ce sujet trivial.
Or le hardware lit les données qu'il va afficher par groupe de 16 pixels. A chaque fois, il tient compte des décalages spécifiés dans BPLCON1 pour retarder plus ou moins l'affichage de ce groupe. Que se passerait-il si la valeur de ces décalages devait être réduite d'un groupe à un autre ? En particulier, le hardware n'afficherait-il pas plus tôt le second groupe, écrasant les derniers pixels du premier ?
C'est exactement ce qui se passe. Et comme dissimuler des colonnes de pixels dans l'image produit une réduction de la largeur de cette dernière, il y a lieu de parler de zoom horizontal hardware :
Encore faut-il régler une question pratique : quand et comment modifier la valeur des décalages dans BPLCON1 ? Comme cela vient d'être suggéré, il faut que l'écriture dans BPLCON1 soit synchronisée sur le cycle de lecture et d'affichage des groupes de pixels par le hardware.
Chacun sait qu'il est toujours possible d'attendre une position du faisceau d'électron (le raster) à l'écran, et de modifier le contenu d'un registre hardware à cet instant. Cette manœuvre peut être réalisée au CPU, mais s'il fallait réaliser le zoom hardware de cette manière, il serait impossible de rien faire d'autre durant une trame.
Fort heureusement, le Copper est là, qui exécute sa Copper list parallèlement. Pourquoi ne pas utiliser ses instructions WAIT et MOVE, détaillées ici, pour parvenir au résultat souhaité ?
Le programme zoom1.s en donne une illustration. L'idée va être d'attendre la position à laquelle on souhaite dissimuler un certain nombre des derniers pixels d'un groupe donné.
Mais où attendre ? A vrai dire, il y aurait deux solutions : utiliser un WAIT puis un MOVE pour modifier BPLCON1. Ou alors, comme dans le cas d'un effet plasma, enchaîner les MOVE en sachant qu'un MOVE prend 8 pixels en basse résolution pour être exécuté, dont un MOVE pour modifier BPLCON1. Dans les faits, seule la seconde solution est praticable.
Pourquoi ? Parce que c'est comme ça. Tous les coders qui utilisent le zoom horizontal hardware le font ainsi. Pourquoi, encore ? Parce qu'il faudrait vraiment, mais alors vraiment, se casser la tête pour parvenir à calculer précisément la position horizontale à spécifier dans le WAIT pour que le MOVE soit exécuté au bon moment - et encore, il n'est pas dit que ce serait possible. A ceux qui se lamenteraient de ce manque de rigueur, disons que pour aller au fond des choses, il faudrait savoir expliquer à tout instant le rapport entre la position horizontale du faisceau d'électron, telle qu'elle est formulée dans un WAIT du Copper, et la valeur horizontale telle qu'elle figure dans DDFSTRT. Or si c'est certainement possible, cela reste à faire...
Ainsi le Copper est programmé pour attendre le début de chaque ligne à la position horizontale $3D, empiriquement déterminée, et procéder à 40 MOVE dont certains vont modifier la valeur de BPLCON1 en réduisant le retard initial, eux aussi empiriquement déterminés.
Il faut noter qu'une telle sollicitation du Copper contraint le nombre de bitplanes, donc le nombre de couleurs à l'écran. La raison en a déjà été présentée ici : au-delà de 4 bitplanes, le hardware vole des cycles au Copper, si bien que ce dernier perd la possibilité d'exécuter autant de MOVE par ligne :
Dans zoom1.s, la partie de la Copper list concernant le zoom (après une initialisation de BPLCON1 à $00FF toutefois) se présente ainsi :
;Zoom move.w #ZOOM_Y<<8,d0 move.w #ZOOM_DY-1,d1 _zoomLines: ;Attendre le début la ligne move.w d0,d2 or.w #$00!$0001,d2 move.w d2,(a0)+ move.w #$8000!($7F<<8)!$FE,(a0)+ ;Initialiser BPLCON1 avec une retard de 15 pixels ($00FF) move.w #BPLCON1,(a0)+ move.w #$00FF,(a0)+ ;Attendre la position sur la ligne correspondant au début de l'affichage (position horizontale $3D dans un WAIT) move.w d0,d2 or.w #ZOOM_X!$0001,d2 move.w d2,(a0)+ move.w #$8000!($7F<<8)!$FE,(a0)+ ;Enchaîner des MOVE qui ne font rien jusqu'à celui qui doit passer le retard à ZOOM_BPLCON1 IFNE ZOOM_MOVE ;Car ASM-One plante sur un REPT dont la valeur est 0... REPT ZOOM_MOVE move.l #ZOOM_NOP,(a0)+ ENDR ENDC ;Modifier BPLCON1 pour passer le retard à ZOOM_BPLCON1 move.w #BPLCON1,(a0)+ move.w #ZOOM_BPLCON1,(a0)+ ;Enchaîner des MOVE qui ne font rien jusqu'à la fin de la ligne IFNE 39-ZOOM_MOVE ;Car ASM-One plante sur un REPT dont la valeur est 0... REPT 39-ZOOM_MOVE move.l #ZOOM_NOP,(a0)+ ENDR ENDC ;Passer à la ligne suivante de la bande de lignes zoomées addi.w #$0100,d0 dbf d1,_zoomLines ;Réinitialiser BPLCON1 ($00FF) pour la fin de l'écran move.w #BPLCON1,(a0)+ move.w #$00FF,(a0)+
La Copper list contient alors notamment un bloc suivant par ligne N, ici encore présenté en pseudo-code pour en faciliter la lecture :
WAIT ($00, N) MOVE #$00FF, BPLCON1 WAIT ($3D, N) REPT ZOOM_MOVE MOVE #$000, ZOOM_NOP ENDR MOVE #ZOOM_BPLCON1, BPLCON1 REPT 39-ZOOM_MOVE MOVE #$000, ZOOM_NOP ENDR