Suivez-nous sur X

|
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
ALL
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
A propos d'Obligement
|
|
David Brunet
|
|
|
|
Programmation : Assembleur - Programmer un défilement sinusoïdal ("sine scroll") sur Amiga (partie 1)
(Article écrit par Yragael et extrait de www.stashofcode.fr - juin 2017)
|
|
Introduction
Un des effets les plus mobilisés par les codeurs sur Amiga a été le défilement sinusoïdal ("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 dans cette intro du groupe Falon :
Le meilleur 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 processeur. Pour étendre l'effet, nous déchargerons le processeur
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 programmé en assembleur 68000,
et encore moins pour attaquer le matériel de l'Amiga.
Cliquez ici pour télécharger l'archive contenant
le code et les données du programme présenté dans cet article. 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 programmé la liste Copper 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.
- 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 programmer le défilement sinusoïdal
présenté ici se trouvent en ligne :
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 programmer 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 via 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 périphérique
logique 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-la. 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
disquette 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 :
- Cliquer 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 boîte 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 100 ko,
par exemple. Dans le menu "Assembler", rendons-nous dans "Processor" et sélectionnons "68000", car nous
allons programmer 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 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ésélectionnons "Save marks".
Notez que pour accéder encore plus vite à l'environnement de développement ainsi mis en place, nous pouvons
presser 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 huit 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 processeur
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 processeur. 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 ajouter un octet ("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 au système d'exploitation
ASM-One permet de générer un exécutable destiné à être exécuté dans le contexte du système d'exploitation.
Toutefois, notre code ne va pas s'appuyer sur le système d'exploitation. En fait, nous allons même chercher
à l'écarter pour nous accaparer le contrôle total du matériel. Notre seule concession au système d'exploitation
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, le système d'exploitation
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 liste Copper
move.l #COPSIZE,d0
move.l #$10002,d1
movea.l $4,a6
jsr -198(a6)
move.l d0,copperlist
;Idem pour les plans de bits
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)
|
Remarque de 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 (par exemple, 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 du système d'exploitation
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 décalage
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 liste Copper, pour trois plans de bits - nous allons faire
du triple tampon mémoire -, 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 mémoire 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 le système d'exploitation. En effet, ce dernier pourrait
parfois avoir installé ou permis d'installer du code exécuté lorsqu'un événement matériel survient. Par
exemple, quand le faisceau d'électrons a terminé de balayer l'écran, le matériel génère un événement matériel
VERTB. Cet événement se traduit par une interruption de niveau 3 du processeur. Le processeur 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 matérielles, 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 matérielles génèrent des interruptions
;de niveau 1 à 6 du processeur 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 processeur
sur une instruction RTE. C'est que le processeur ne dispose pas que des vecteurs des interruptions de niveau N
(de 0 à 7), mais de 255 vecteurs, par exemple, le vecteur 5 - le code sur lequel il pointe est appelé en cas
de division par zéro. Toutefois, ce serait superflu.
Remarque de StingRay/Scoopex : "Les processeurs 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 matériel
C'est l'occasion de préciser la manière dont notre code va dialoguer avec le matériel. Ce sera via des
registres 16 bits résidant à l'adresse $DFF000 plus un décalage 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 décalages. Par exemple :
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 matérielles soient pendantes. En effet, qu'il dispose de la possibilité
d'interrompre le processeur ou non, le matériel 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 matériel 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
le système d'exploitation. C'est aussi pour cela qu'on parle de "metal bashing". Enfin, voilà : nous
avons désormais le contrôle total du matériel. 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 liste Copper, à fournir sous la
forme d'une séquence de codes opérations, 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
plans de bits superposés tels que la lecture du bit aux coordonnées (x, y) dans le plan de bits 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 plans de bits : N plans de bits pour 2^N couleurs. Ici, nous allons afficher
un plan de bits, 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 plans de bits. 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 matériel 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és 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 plans de bits 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 plan
de bits 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 140 ns et il met 1/50e 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 plans de bits 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)<alt;4))>>1)&$00FC,(a0)+
|
Les données des pixels résident dans le plan de bits. 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 matériel incrémente le contenu de BPLxPTH et BPLxPTL tandis qu'il lit les données du plan de bits
lors de l'affichage d'une ligne. Arrivé à la fin de cette ligne, le matériel 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 plans de bits impairs, et BPL2MOD celui des plans de bits
pairs. Pour l'heure, seul BPL1MOD est utilisé car il n'y a qu'un plan de bits, le plan de bits 1
qui est donc un plan de bits impair. Ce modulo est à 0, car le plan de bits 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 plans de bits, le matériel 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 jeu de puces AGA, mais le code que nous écrivons est destiné
à fonctionner sur un Amiga 500 doté du jeu de puces 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 liste Copper quand il rencontre une instruction WAIT impossible :
Nous reviendrons sur l'écriture d'un WAIT lorsque nous chercherons à rajouter les effets d'ombre et de miroir.
La liste Copper étant rédigée, nous pouvons demander au Copper de l'exécuter. Cela s'effectue en deux temps :
- Fournir l'adresse de la liste Copper 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 matériel de lire les données des plans de bits 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 processeur (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 matériel étant configuré, nous pouvons maintenant rentrer dans le code qui génère ce qu'il doit afficher...
|