Un des effets les plus mobilisés par les coders sur Amiga a été le sine scroll, c'est-à-dire le défilé de texte déformé en modifiant l'ordonnée de colonnes de pixels successives selon un sinus, comme par exemple dans cette intro du groupe Falon :
Le must est le one pixel sine scroll, où chacune de ces colonnes est affichée à une ordonnée spécifique. Toutefois, produire un tel effet est très consommateur en temps de calcul, comme nous le montrerons en y procédant d'abord au seul moyen du CPU. Pour étendre l'effet, nous déchargerons le CPU en sollicitant deux coprocesseurs graphiques : le Blitter et le Copper.
Cet article peut être lu par n'importe qui, ayant été rédigé pour qui n'aurait jamais codé en assembleur 68000, et encore moins pour attaquer le hardware de l'Amiga.
Cliquez ici pour télécharger l'archive contenant le code et les données du programme présenté ici.
Cette archive contient plusieurs sources :
- sinescroll.s est la version de base dont il sera question jusqu'à ce que nous optimisions ;
- sinescroll_final.s est la version optimisée de la version de base ;
- sinescroll_star.s est la version enjolivée de la version optimisée.
Cet article est le premier d'une série de cinq. Nous allons voir 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.
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 : Suppression du positionnement de BLTPRI dans DMACON pour améliorer les performances.
Mise à jour du 17/07/2017 : Désactivation de la fonctionnalité Save marks dans ASM-One pour faciliter l'édition mixte PC / Amiga.
Click here to read this article in english.
Mise à jour du 30/05/2018 : Ceux qui utilisent Notepad++ peuvent cliquer ici pour récupérer une version améliorée de l'UDL 68K Assembly (v4).
Mise à jour du 16/06/2018 : StingRay / Scoopex a permis de corriger des co(q)uilles, d'ajuster du vocabulaire et d'ajouter des détails pour la comptabilité. Merci à lui !
Installer un environnement de développement
Précisons d'emblée que toutes les documentations mobilisées pour coder le sine scroll présenté ici se trouvent en ligne :
- l'Amiga Hardware Reference Manual, qui détaille le fonctionnement et la manière de programmer le hardware de l'Amiga ;
- le M68000 Family Programmer's User Manual, qui détaille les instructions du 68000 ;
- le M68000 8-/16-/32-Bit Microprocessors User's Manual, qui détaille le temps d'exécution de ces dernières ;
- le manuel d'ASM-One, dont une version plus récente au format AmigaGuide se trouve toutefois dans l'archive de l'outil pour être consultée sur Amiga.
Il en est de même pour les outils, à commencer par l'émulateur Amiga. En effet, nul besoin de récupérer un Amiga sur eBay pour coder de nos jours sur cette machine. Nous utiliserons l'excellent émulateur WinUAE auquel nous demanderons de simuler un disque dur à partir d'un répertoire du PC. Cela nous permettra d'éditer le code dans un éditeur de texte sous Windows et de le charger pour l'assembler et le tester avec ASM-One s'exécutant sur un Amiga 1200 émulé par WinUAE.
Après avoir téléchargé et installé WinUAE, nous devons récupérer la ROM et le système d'exploitation, à savoir le Kickstart et le Workbench dans leurs versions 3.1. Kickstart et Workbench sont encore soumis à des droits. Ils sont commercialisés pour quelques dizaine d'euros par Amiga Forever.
Dans WinUAE, commençons par recréer la configuration d'un Amiga 1200 dans Hardware :
- dans CPU and FPU, sélectionnons un 68020 ;
- dans Chipset, sélectionnons AGA ;
- dans ROM, sélectionnons le Kickstart 3.1 ;
- dans RAM, optons pour 2 Mo de Chip pas de Slow ;
- dans CD & Hard drives, cliquons sur Add Directory or Achives... et ajoutons un device nommé DH0: renvoyant à un répertoire de notre PC où nous trouverons les fichiers de cette simulation de disque dur.
Cette configuration étant créée, enregistrons-là. Pour cela, cliquons sur Hardware, donnons-lui un nom et cliquons sur Save. Par la suite, nous pourrons recharger à tout instant la configuration en double-cliquant dessus.
Dans la même rubrique, rendons-nous dans Floppy Drives pour simuler l'insertion dans le lecteur de disquettes DF0: de la première disquette du Workbench - celle libellée Install 3.1. A cette occasion, réglons la vitesse d'émulation du lecteur de disquettes au maximum (glissière tout à gauche, sur Turbo) pour ne plus perdre de temps.
Nous pouvons alors cliquer sur Reset pour lancer l'émulation.
Une fois le Workbench chargé à partir de la disquette, il s'agit de l'installer sur le disque dur pour s'épargner de long temps de chargement. Double cliquons sur l'icône de la disquette Install 3.1, puis sur celle de son répertoire Install, et enfin sur celle de la version de l'installation que nous souhaitons. Laissons-nous ensuite guider dans le processus d'installation du Workbench sur le disque dur :
Une fois le système d'exploitation installé sur disque dur, nous devons installer l'environnement de développement. Pour assembler le source et le lier avec les données au sein d'un exécutable, nous utiliserons ASM-One. Comme tous les fichiers évoqués par la suite, il nous suffit d'en télécharger l'archive sur votre PC et de déposer le contenu de cette dernière dans un sous-répertoire du répertoire servant à émuler le disque dur. Souvenons-nous que dans le Workbench, les seuls répertoires visibles sont ceux dotés d'un fichier .info. La solution la plus simple consiste donc à créer le répertoire depuis le Workbench - cliquer du bouton droit sur la barre des tâches en haut de l'écran, vous vous souvenez ?
Pour utiliser ASM-One, nous devons :
- Cliquez ici pour télécharger reqtools.library et copier ce fichier dans le répertoire Libs. Cette bibliothèque de fonctions est utilisée par ASM-One pour nous proposer une boite de dialogue qui facilite la navigation dans le système de fichiers.
- Utiliser une commande du Shell (que nous trouvons dans le répertoire System) pour assigner SOURCES: au répertoire contenant le code et les données (par exemple : assign SOURCES: DH0:sinescroll). Pour nous éviter cela, nous pouvons écrire cette ligne dans un fichier User-Startup à stocker dans le répertoire S.
Après avoir lancé ASM-One, allouons un espace de travail en mémoire quelconque (Chip ou Fast) de 100Ko, par exemple. Dans le menu Assembler, rendons-nous dans Processor et sélectionnons 68000, car nous allons coder pour Amiga 500. Saisissons la commande R (Read) pour charger le source.
Pour assembler et tester, deux solutions :
- Si nous souhaitons déboguer, pressons les touches Amiga (droite) + Maj (droite) + D, la touche Amiga (droite) étant émulée à l'aide de la touche Windows (droite). Nous accédons ainsi au débogueur, d'où nous pouvons exécuter ligne par ligne en pressant la touche de direction vers le bas ou exécuter globalement en pressant les touches Amiga (droite) + R.
- Si nous ne souhaitons pas déboguer, nous pouvons toujours presser les touches Amiga (droite) + Maj (droite) + A ou saisir la commande A (Assemble) pour assembler, puis saisir la commande J (Jump) pour lancer l'exécution.
Nous n'utiliserons pas plus de fonctionnalités d'ASM-One par la suite, sinon pour générer un exécutable. C'est que pour écrire du code, nous pouvons nous dispenser d'ASM-One : utilisons un éditeur de texte sous Windows qui permet d'enregistrer un fichier texte encodé en ANSI, comme par exemple l'excellent Notepad++, et chargeons le fichier dans ASM-One pour l'assembler et l'exécuter quand nous le souhaitons.
Malgré tout, pour écrire du code aussi bien dans ASM-One que dans Notepad++, car cela peut se révéler ponctuellement pratique, désactivons la sauvegarde des marques qui introduit des caractères spéciaux en tête de fichier. Dans le menu Project d'ASM-One, rendons-nous dans Preferences et déselectionnons Save marks.
Notez que pour accéder encore plus vite à l'environnement de développement ainsi mis en place, nous pouvons presser sur la touche F12 après avoir chargé ASM-One et alloué un espace de travail en mémoire. Dans l'interface de WinUAE qui apparaît, cliquons sur Miscellaneous dans Host, puis sur Save State... afin de sauvegarder l'état. Par la suite, quand nous aurons démarré WinUAE, il nous suffira de charger votre configuration d'Amiga 1200, de cliquer sur Load State... pour charger l'état, puis de cliquer sur OK pour retrouver l'Amiga 1200 dans l'état en question - nous pouvons même associer le chargement de l'état à celui de la configuration. Pratique !
Se familiariser avec l'assembleur 68000
Le 68000 comporte 8 registres de données (D0 à D7) et autant de registres d'adresses (A0 à A7, ce dernier servant toutefois de pointeur de pile). Son jeu d'instructions est tout à fait étendu, mais nous n'en utiliserons qu'un tout petit ensemble, notre malheureuse ignorance pouvant opportunément passer ici pour une bienheureuse simplicité.
Les instructions du 68000 peuvent connaître de multiples variantes. Plutôt qu'un fastidieux passage en revue de toutes les variantes des instructions de notre jeu pourtant limité, ces quelques exemples devraient vous suffire pour vous y retrouver :
Instructions | Description |
Stockage | |
MOVE.W $1234,A0 | Stocke dans A0 la valeur 16 bits contenue à l'adresse $1234 |
MOVE.W $1234,D0 | Idem avec D0 |
MOVE.W #$1234,A0 | Stocke dans A0 la valeur 16 bits $1234 |
MOVE.W #$1234,A0 | Idem avec D0 |
LEA $4,A0 | Stocke dans A0 la valeur 32 bits $4 |
LEA variable,A0 | Stocke dans A0 l'adresse de l'octet précédé du libellé "variable" |
LEA 31(A0),A1 | Stocke dans A1 le résultat de l'addition de 31 au contenu de A0 |
LEA 31(A0,DO.W),A1 | Stocke dans A1 le résultat de l'addition la valeur 32 bits contenue dans A0, de la valeur 16 bits contenue dans D0 et enfin de 31 |
MOVE.L variable,A0 | Stocke dans A0 la valeur 32 bits qui suit le libellé "variable" |
MOVE.L variable,D0 | Idem avec D0 |
CLR.W D0 | Stocke dans D0 la valeur 16 bits 0 |
MOVEQ #-7,D0 | Stocke dans D0 la valeur 8 bits -7 étendue sur 32 bits |
MOVE.W D0,D1 | Stocke dans D1 la valeur 16 bits contenue dans D0 |
MOVE.B (A0),D0 | Stocke dans D0 la valeur 8 bits contenue à l'adresse contenue dans A0 |
MOVE.L (A0)+,D0 | Stocke dans D0 la valeur 32 bits contenue à l'adresse contenue dans A0, puis ajoute 4 à la valeur contenue dans A0 pour adresser la valeur 32 bits suivante |
MOVE.B (A0,D0.W)+,D1 | Stocke dans D1 la valeur 32 bits contenue à l'adresse résultant de l'addition de l'adresse contenue dans A0 et de la valeur 16 bits contenue dans D0, puis ajoute 1 à la valeur contenue dans A0 pour adresser la valeur 8 bits suivante |
Sauts | |
JMP destination | Saute à l'instruction précédée du libellé "destination" sans possibilité de retour |
BRA destination | Comme JMP (pour faire simple) |
BNE destination | Comme JMP, mais uniquement si le drapeau Z (zéro) du registre de conditions interne du CPU n'est pas positionné |
BEQ destination | Comme JMP, mais uniquement si Z est positionné |
BGE destination | Comme JMP, mais uniquement si Z ou C (retenue) est positionné |
BLE destination | Comme JMP, mais uniquement si Z est positionné ou C ne l'est pas |
BGT destination | Comme JMP, mais uniquement si C est positionné |
DBF D0,destination | Soustrait 1 à valeur contenue dans D0 et saute à l'instruction précédée du libellé "destination" si jamais le résultat n'est pas -1 |
JSR destination | Comme JMP, mais avec possibilité de retour |
RTS | Saute à l'instruction suivant le dernier JSR exécuté |
Calculs | |
BTST #4,D0 | Teste la valeur du bit 4 de valeur contenue dans D0 |
BCLR #6,D0 | Passe le bit 6 de la valeur contenue dans D0 à 0 |
LSL.W #1,D0 | Décale de 1 bit vers la gauche la valeur 16 bits contenue dans D0 (multiplication non signée par 2^1=2) |
LSR.B #4,D0 | Décale de 4 bits vers la gauche la valeur 8 bits contenue dans D0 (division non signée par 2^4=16) |
ASL.W #1,d0 | Comme LSL, mais en préservant le bit de signe (multiplication signée par 2^1=2) |
ASR.B #4,D0 | Comme LSR, mais en préservant le bit de signe (division signée par 2^4=16) |
SWAP D0 | Intervertit la valeur 16 bits de poids fort (bits 31 à 16) et la valeur 16 bits de poids faible (bits 15 à 0) contenues dans D0 |
CMP.W D0,D1 | Compare la valeur 16 bits contenue dans D1 à la valeur 16 bits contenue dans D0 |
ADDQ.W 2,D0 | Additionne la valeur 3 bits 2 à la valeur 16 bits contenue dans D0 |
ADD.B D0,D1 | Additionne la valeur 8 bits contenue dans D0 à la valeur contenue dans D1 |
SUB.L D0,D1 | Soustrait la valeur 32 bits contenue dans D0 à la valeur contenue dans D1 |
Ainsi, il est possible de limiter les opérations sur les données à une valeur 8 bits, 16 bits ou 32 bits. Quand un registre est impliqué dans une telle opération, c'est alors l'octet de poids faible, le mot de poids faible ou l'intégralité de la valeur qu'il contient qui est manipulée. Par exemple :
move.l #$01234567,d0 ;D0=$01234567 moveq #-1,d1 ;D1=$FFFFFFFF move.b d0,d1 ;D1=$FFFFFF67 move.w d1,d0 ;D0=$0123FF67
L'exécution d'une instruction débouche sur une mise à jour des drapeaux du registre de conditions interne du CPU. C'est assez intuitif. Par exemple :
move.b value,d0 ;D0=[valeur] beq _valueZero ;Sauter en _valueZero si [valeur] vaut 0 btst #2,d0 ;Tester le bit 2 de [valeur] bne _bit2NotZero ;Sauter en _bit2NotZero si le bit vaut 0 ;... _valueZero: ;... _bit2NotZero: ;...
La seule subtilité que nous nous autoriserons, ce sera de limiter le temps de calcul en recourant à des opérations binaires plutôt qu'à des multiplications ou des divisions. Par exemple :
move.l #157,d0 ;D0=157 move.l d0,d1 ;D1=157 lsl.w #5,d0 ;D0=157*2^5 donc D0=157*32 lsl.w #3,d1 ;D1=157*2^3 donc D1=157*8 add.w d0,d1 ;D0=157*2^5+157*2^3 donc D0=157*40
Nous utiliserons très peu de variables. Ces dernières sont déclarées à la fin du code sur le modèle suivant :
value8: DC.B $12 EVEN ;Pour que l'adresse qui suit soit paire value16: DC.W $1234 value32: DC.L $12345678
Comme indiqué, EVEN indique à ASM-One qu'il doit introduire un octet de padding entre $12 et $1234 pour que cette dernière valeur se trouve à une adresse paire. Pourquoi ? Car le 68000 ne peut lire de valeurs 16 bits ou 32 bits qu'à des adresses paires.
Dire au revoir à l'OS
ASM-One permet de générer un exécutable destiné à être exécuté dans le contexte de l'OS. Toutefois, notre code ne va pas s'appuyer sur l'OS. En fait, nous allons même chercher à l'écarter pour nous accaparer le contrôle total du hardware. Notre seule concession à l'OS sera de ne pas taper n'importe où en mémoire, et donc de lui demander de nous allouer les espaces qui nous sont nécessaires, espaces que nous lui demanderons de libérer à la fin. Entretemps, l'OS sera complètement court-circuité.
;Empiler les registres movem.l d0-d7/a0-a6,-(sp) ;Allouer de la mémoire en Chip mise à 0 pour la Copper list move.l #COPSIZE,d0 move.l #$10002,d1 movea.l $4,a6 jsr -198(a6) move.l d0,copperlist ;Idem pour les bitplanes move.l #(DISPLAY_DX*DISPLAY_DY)>>3,d0 move.l #$10002,d1 movea.l $4,a6 jsr -198(a6) move.l d0,bitplaneA move.l #(DISPLAY_DX*DISPLAY_DY)>>3,d0 move.l #$10002,d1 movea.l $4,a6 jsr -198(a6) move.l d0,bitplaneB move.l #(DISPLAY_DX*DISPLAY_DY)>>3,d0 move.l #$10002,d1 movea.l $4,a6 jsr -198(a6) move.l d0,bitplaneC ;Idem pour la police de caractères move.l #256<<5,d0 move.l #$10002,d1 movea.l $4,a6 jsr -198(a6) move.l d0,font16 ;Couper le système movea.l $4,a6 jsr -132(a6)
StingRay / Scoopex : "Veillez à appeler LoadView(0) suivi de deux appels à WaitTOF() avant de désactiver les DMA/interruptions et autre, car cela permettra de faire fonctionner votre code sur des écrans non-natifs (ie: RTG). C'est plutôt ennuyeux pour les utilisateurs qui ont des cartes graphiques d'avoir à désactiver leur carte pour seulement faire tourner une démo. :)" Faites attention !
AllocMem() et Forbid() sont les deux fonctions de la bibliothèque Exec de l'OS utilisées ici. Pour appeler une fonction d'Exec, nous devons renseigner un certain nombre de registres où cette fonction s'attend à pouvoir lire des paramètres, puis effectuer un saut au bon offset d'une table d'indirections - une table de JMP -, table dont l'adresse réside à l'adresse $4. La fonction retourne ses résultats dans un certain nombre de registres. Ainsi, AllocMem () retourne l'adresse du bloc de mémoire alloué dans D0.
AllocMem() sert ici à allouer de la mémoire pour la Copper list, pour trois bitplanes - nous allons faire du triple buffering -, et pour la police de caractères - nous allons fabriquer un police 16x16 à partir d'une police 8x8. Tous ces espaces sont demandés en Chip, la seule mémoire à laquelle le Copper et le Blitter ont accès, par opposition à la mémoire Fast.
Il ne suffit pas d'appeler Forbid() pour couper l'OS. En effet, ce dernier pourrait parfois avoir installé ou permis d'installer du code exécuté lorsqu'un événement hardware survient. Par exemple, quand le faisceau d'électrons a terminé de balayer l'écran, le hardware génère un événement hardware VERTB. Cet événement se traduit par une interruption de niveau 3 du CPU. Le CPU suspend ses travaux pour exécuter le code dont l'adresse est spécifiée à l'entrée numéro 27 de sa table de vecteurs d'interruption - le vecteur d'interruption 27 -, soit l'adresse $6C :
Si notre code devait utiliser de telles interruptions hardware, il faudrait d'abord détourner les vecteurs, c'est-à-dire s'assurer qu'ils pointent sur une instruction RTE :
;Détourner les vecteurs d'interruption (code). Les interruptions hardware génèrent des interruptions de niveau 1 à 6 du CPU correspondant aux vecteurs 25 à 30 pointant sur les adresses $64 à $78 REPT 6 lea vectors,a1 REPT 6 move.l (a0),(a1)+ move.l #_rte,(a0)+ ENDR ;... ;Détourner les vecteurs d'interruption (données) _rte: rte vectors: BLK.L 6 ;Pour s'épargner une allocation mémoire
Pour être encore plus brutal, il serait possible de faire pointer tous les vecteurs d'interruptions du CPU sur une instruction RTE. C'est que le CPU ne dispose pas que des vecteurs des interruptions de niveau N (de 0 à 7), mais de 255 vecteurs, comme par exemple le vecteur 5 - le code sur lequel il pointe est appelé en cas de division par zéro. Toutefois, ce serait superflu.
StingRay / Scoopex : "les CPU 68010+ permettent de relocaliser l'adresse de base du vecteur utilisé par le VBR, ce qui signifie que ce code ne fonctionnera pas sur les machines aussitôt que le VBR aura été relocalisé" Faites attention !
Dans le cas présent, nous n'utiliserons pas ces interruptions, si bien qu'il suffit de les inhiber. Pour cela, nous devons lire dans INTENAR pour récupérer l'état des interruptions activées, sauvegarder cet état, puis inhiber les interruptions en écrivant dans INTENA.
Les registres du hardware
C'est l'occasion de préciser la manière dont notre code va dialoguer avec le hardware. Ce sera via des registres 16 bits résidant à l'adresse $DFF000 plus un offset pair. Par exemple, INTENAR se trouve à l'adresse $DFF01C. Nous appliquerons une recommandation du manuel pour limiter les erreurs de saisie et faciliter la lecture. Nous stockerons $DFF000 dans un registre d'adresse quelconque - ce sera A5 - et nous adresserons les registres à l'aide de constantes dont les valeurs sont les offsets. Par exemple :
INTENA=$09A
Chaque registre est très spécifique. La signification de chacun de ses bits est détaillée dans l'Amiga Hardware Reference Manual, un vrai manuel, fort bien rédigé par des auteurs maîtrisant complètement leur sujet. Le défi de cet article est de ne pas recopier le contenu de ce manuel indispensable. Il est donc recommandé de se reporter à l'annexe A de ce dernier pour consulter ce qui est dit du registre, puis de continuer la lecture.
|
Il est aussi possible que des interruptions hardware soient pendantes. En effet, qu'il dispose de la possibilité d'interrompre le CPU ou non, le hardware signale les raisons pour lesquels il souhaiterait l'interrompre dans INTREQ - qui forme avec INTREQR un couple similaire à celui vu à l'instant. Toujours pour ménager un jour la possibilité d'utiliser des interruptions - ce qui, répétons-le, ne sera pas le cas ici -, nous devons lire dans INTREQR l'état des requêtes d'interruption, puis acquitter ces requêtes en écrivant dans INTREQ pour ne pas les confondre avec celles que le hardware présente par la suite.
Un dernier registre doit être lu : c'est DMACONR. Sur Amiga, les coprocesseurs disposent d'accès directs à la mémoire, ou DMA. Ici encore, c'est quelque chose que nous entendons contrôler pour limiter les accès DMA à ceux qui nous seront utiles. Nous devons donc lire l'état des canaux dans DMACONR pour récupérer l'état des canaux DMA activés, sauvegarder cet état, puis couper les canaux en écrivant dans DMACON - pour commencer, nous les coupons tous.
INTENA, INTREQ et DMACON fonctionnent sur le même modèle : pour inhiber une interruption, acquitter une interruption ou couper un canal DMA, nous devons écrire un mot dont le bit 15 est à 0 et le bit correspondant à l'interruption ou au canal est à 1.
Tout cela conduit à écrire :
- $7FFF dans INTENA. Nous aurions pu nous contenter de désactiver le bit INTEN, mais si dans le futur vous souhaitez utiliser des interruptions, vous ne voudrez pas avoir à y revenir pour désactiver celles qui ne vous intéressent pas avant de réactiver INTEN et celles qui vous intéressent.
- $7FFF dans INTREQ. Ce registre contient lui aussi un bit INTEN.
- $07FF dans DMACON. La remarque qui vaut pour INTEN vaut ici pour le bit DMAEN. Par paresse, nous aurions pu écrire $7FFF, mais les bits 11 à 14 ne servent à rien en écriture.
;Couper les interruptions hardware et les DMA lea $dff000,a5 move.w INTENAR(a5),intena move.w #$7FFF,INTENA(a5) move.w INTREQR(a5),intreq move.w #$7FFF,INTREQ(a5) move.w DMACONR(a5),dmacon move.w #$07FF,DMACON(a5)
Tout cela est-il bien propre ? Certainement pas. Il n'y a pas de manière propre pour couper rapidement l'OS. C'est aussi pour cela qu'on parle de metal bashing. Enfin, voilà : nous avons désormais le contrôle total du hardware. Commençons par configurer l'affichage.
Configurer l'affichage
Dans un précédent article, nous avons présenté les coprocesseurs graphiques de l'Amiga, dont le Copper, qui contrôle l'affichage. Nous avons expliqué que le Copper se programme via une liste d'instructions, la Copper list, à fournir sous la forme d'une séquence d'opcodes, des longs (32 bits) écrits en hexadécimal. Le Copper comprend trois instructions (WAIT, MOVE et SKIP), mais c'est uniquement MOVE que nous utiliserons pour l'heure pour demander au Copper d'écrire certaines valeurs dans les registres qui contrôlent l'affichage.
Justement, nous avions aussi expliqué comment cet affichage fonctionne. Il est à base de bitplanes, plans de bits superposés tels que la lecture du bit aux coordonnées (x, y) dans le bitplane N donne le bit N-1 de l'index de la couleur du pixel correspondant dans la palette, etc. Le nombre de couleurs est donc déterminé par le nombre de bitplanes : N bitplanes pour 2^N couleurs. Ici, nous allons afficher un bitplane, donc en deux couleurs - couleur de fond comprise.
Pour nous y retrouver facilement, définissons quelques constantes :
DISPLAY_DEPTH=1 DISPLAY_DX=320 DISPLAY_DY=256 DISPLAY_X=$81 DISPLAY_Y=$2C
Les paramètres d'affichage suivants doivent être spécifiés :
- La résolution. Les pixels seront affichés en basse résolution, ce qui ne requiert aucun positionnement de bit particulier dans un quelconque registre.
- Le nombre de bitplanes. Les bits BPUx de BPLCON0 doivent donner ce nombre, c'est-à-dire DISPLAY_DEPTH.
- L'affichage en couleurs. Le bit COLOR de BPLCON0 doit être positionné.
- La surface vidéo de pixels à balayer. DIWSTRT doit contenir les coordonnées de son angle supérieur gauche, et DIWSTOP celles de son angle inférieur droit. Ces coordonnées sont exprimées en pixels dans un repère très particulier, celui du tube cathodique. Généralement, la surface démarre en ($81, $2C) et s'étend sur DISPLAY_DX pixels horizontalement et DISPLAY_DY pixels verticalement. Noter qu'en raison du nombre de bits limités dans DIWSTOP, nous devons soustraire 256 aux coordonnées qu'on y écrit.
- Les coordonnées horizontale à partir desquelles commencer et cesser de lire les données des pixels à afficher. Ces abscisses sont exprimées dans le même repère que celles des angles de la surface vidéo dans DIWSTRT et DIWSTOP. Le hardware lit les données des pixels par paquets de 16 pixels. Par ailleurs, il s'écoule un peu de temps entre le moment où le hardware commencer à lire ces données et celui où les pixels correspondants commencent à être affichés. C'est pourquoi, sous condition que DISPLAY_DX soit multiple de 16, la lecture des données doit débuter en (DISPLAY_X-17)>>1.
Le tracé des pixels
Ce qu'il faut comprendre, c'est qu'une fois la résolution décidée - basse ou haute résolution horizontale, avec ou sans entrelacement vertical - le débit du faisceau d'électrons est constant. En effet, il balaie toujours toute la surface du tube cathodique, traçant les pixels en frappant avec plus ou moins d'intensité les régions rouge, verte et bleue des luminophores sur une certaine longueur, une succession de luminophores formant un pixel. Tout ce que l'Amiga peut faire, c'est de demander au faisceau de ne frapper que les luminophores d'une certaine surface du tube, en frappant avec différentes intensité les points rouge, vert et bleu qui composent ces luminophores.
DIWSTRT et DIWSTOP permettent de contrôler la position et les dimensions de la surface en question. DDFSTRT et DDFSTOP permettent de contrôler les positions à partir desquelles l'Amiga décide de continuer et de cesser de lire les données des bitplanes pour en déduire les intensités de rouge, de vert et de bleu à communiquer au faisceau d'électrons.
Autrement dit, il ne faut pas se faire d'illusions : il n'est pas possible d'afficher toute une ligne d'un bitplane sur une ligne plus ou moins large de l'écran et de procéder pareillement en vertical - une forme de zoom vidéo. Une fois décidées via des bits de BPLCON0, les résolutions horizontale et verticale sont bel et bien figées : quoiqu'il arrive le faisceau d'électron trace un pixel en 140ns et il met 1/50ème de seconde pour tracer tout l'écran sur une certaine largeur et une certaine hauteur.
L'Amiga interagit avec un peintre qui balaie toujours la même surface à la même vitesse, acceptant seulement qu'on modifie à tout instant - enfin, au minimum le temps qu'il trace un pixel - ce qu'il puise dans ses pots de peinture rouge, verte et bleue.
|
Tous les autres paramètres doivent être désactivés. Par exemple, il n'est pas question de retarder l'affichage des bitplanes impairs de quelques pixels horizontalement, si bien que les bits PF1Hx de BPLCON1 sont passés à 0. Ou encore, il n'est pas question d'afficher en haute résolution, si bien que le bit HRES de BPLCON0 est passé à 0.
Ce qui donne :
move.w #DIWSTRT,(a0)+ move.w #(DISPLAY_Y<<8)!DISPLAY_X,(a0)+ move.w #DIWSTOP,(a0)+ move.w #((DISPLAY_Y+DISPLAY_DY-256)<<8)!(DISPLAY_X+DISPLAY_DX-256),(a0)+ move.w #BPLCON0,(a0)+ move.w #(DISPLAY_DEPTH<<12)!$0200,(a0)+ move.w #BPLCON1,(a0)+ move.w #0,(a0)+ move.w #BPLCON2,(a0)+ move.w #0,(a0)+ move.w #DDFSTRT,(a0)+ move.w #((DISPLAY_X-17)>>1)&$00FC,(a0)+ move.w #DDFSTOP,(a0)+ move.w #((DISPLAY_X-17+(((DISPLAY_DX>>4)-1)<<4))>>1)&$00FC,(a0)+
Les données des pixels résident dans le bitplanes. Nous devons donc préciser où ils se trouvent en mémoire en écrivant leurs adresses dans des couples de registres BPLxPTH (16 bits de poids fort de l'adresse) et BPLxPTL (16 bits de poids fort de l'adresse) :
move.l bitplaneA,d0 move.w #BPL1PTL,(a0)+ move.w d0,(a0)+ swap d0 move.w #BPL1PTH,(a0)+ move.w d0,(a0)+
Le hardware incrémente le contenu de BPLxPTH et BPLxPTL tandis qu'il lit les données du bitplane lors de l'affichage d'une ligne. Arrivé à la fin de cette ligne, le hardware ajoute un certain nombre d'octets à ces registres pour adresser les premiers pixels de la ligne suivante : c'est le modulo. BPL1MOD permet de spécifier le modulo des bitplanes impairs, et BPL2MOD celui des bitplanes pairs. Pour l'heure, seul BPL1MOD est utilisé car il n'y a qu'un bitplane, le bitplane 1 qui est donc un bitplane impair. Ce modulo est à 0, car le bitplane fait DISPLAY_DX pixels de large et nous souhaitons afficher DISPLAY_DX pixels par ligne :
move.w #BPL1MOD,(a0)+ move.w #0,(a0)+
Ayant récupéré les bits du pixel courant dans les bitplanes, le hardware peut en déduire l'indice dans une palette de la couleur du pixel en question. Nous spécifions les deux couleurs de notre palette en écrivant leurs valeurs dans COLOR00 et COLOR01 :
move.w #COLOR00,(a0)+ move.w #$0000,(a0)+ move.w #COLOR01,(a0)+ move.w #SCROLL_COLOR,(a0)+
L'Amiga émulé est un Amiga 1200 doté du chipset AGA, mais le code que nous écrivons est destiné à fonctionner sur un Amiga 500 doté du chipset OCS. En ce qui concerne la vidéo, la compatibilité ascendante de l'AGA avec l'OCS est presque parfaite. Nous devons simplement ne pas oublier d'écrire 0 dans FMODE :
move.w #FMODE,(a0)+ move.w #$0000,(a0)+
Enfin, le Copper détecte la fin de la Copper list quand il rencontre une instruction WAIT impossible :
move.l #$FFFFFFFE,(a0)
Nous reviendrons sur l'écriture d'un WAIT lorsque nous chercherons à rajouter les effets d'ombre et de miroir.
La Copper list étant rédigée, nous pouvons demander au Copper de l'exécuter. Cela s'effectue en deux temps :
- fournir l'adresse de la Copper list via COP1LCH et COP1LCL, ce qui peut s'effectuer d'un MOVE.L car comme BPLxPTH et BPLxPTL, ces registres sont contigus ;
- écrire n'importe quelle valeur dans COPJMP1, car c'est un strobe, c'est-à-dire un registre qui déclenche une action dès qu'on en modifie la valeur.
move.l copperlist,COP1LCH(a5) clr.w COPJMP1(a5)
Encore faut-il que le Copper puisse accéder à la mémoire par DMA. C'est l'occasion de rouvrir son canal DMA, mais aussi celui permettant au hardware de lire les données des bitplanes et celui du Blitter. On en profite pour protéger les cycles d'accès à la mémoire du Blitter pour qu'il ne se les fasse pas voler par le CPU (l'Amiga Hardware Reference Manual ne donne pas beaucoup d'explications sur ce sujet...) :
move.w #$83C0,DMACON(a5) ;DMAEN=1, BPLEN=1, COPEN=1, BLTEN=1
Le hardware étant configuré, nous pouvons maintenant rentrer dans le code qui génère ce qu'il doit afficher...