Démo ( 3 )

Par Mike Dbug.

 


 

Re-bonjour.
J'espère que vous avez réussi à configurer le kit, et a compiler le programme de la dernière fois. Nous allons maintenant attaquer la partie théorique.

L'idée de base pour faire une démo (ou bien un jeu rapide), c'est d'avoir une cadence d'affichage (frame rate) suffisamment élevé pour vraiment donner une illusion de mouvement.
Le but ultime, étant de réussir à afficher une image à chaque nouveau balayage vidéo. Dans notre cas, vu que nous sommes européens, et que nous utilisons des télévisions ou bien des moniteurs RVB, cette vitesse est de 50hz.
50 hertz, ca signifie "50 changements d'états par seconde". Pour simplifier si l'on arrive à afficher une nouvelle image 50 fois par seconde, on dira que nous sommes "dans la frame".

Maintenant, le but du jeu est de savoir ce que l'on peut faire en 1/50ème de seconde avec le pôvreu 6502 à 1mhz.
Pour cela, il faut juste savoir compter les cycles.
Les cycles ? Kesako ?

Le microprocesseur est cadencé par une horloge. A chaque incrémentation de l'horloge, le microprocesseur exécute un CYCLE d'opérations. Ces opérations sont en général de ce type:

- lecture d'un octet composant une instruction en mémoire (code opération: NOP, LDA, ... )
- lecture d'un paramètre d'une instruction ( #35, $FFFC )
- lecture/écriture d'un octet en mémoire
et tout ce qui concerne son petit ménage interne (gestion des interruptions, etc...)

Chaque instruction prends un certain temps à s'exécuter. Ce temps dépend d'une part du nombre d'octets composant cette instruction, et d'autre part de la complexité que représente cette instruction.
Une des instructions les plus simples, est NOP. Le NOP occupe 1 octet en mémoire, et prends 2 cycles pour s'exécuter. Le premier cycle, est celui qui correspond au chargement de l'instruction en mémoire, et le second celui du temps pris par l'exécution.
Les instruction plus complexes (branchements, chargements indexés, calculs) demandent plus de temps pour s'exécuter.
Plus l'instruction est complexe, plus elle occupe d'octets, plus elle est lente.

Nous savons donc que NOP prend 2 cycles. Il pourrait être intéressant de savoir combien de NOPs on peut exécuter en 1/50ème de seconde ?

Fréquence du processeur: 1mhz = 1 000khz = 1 000 000hz.
Fréquence de l'affichage = 50hz
Nombre de cycles=1 000 000 / 50 = 20000 cycles/frame

Et en 60hz ? (On peut commuter l'ORIC en 60hz avec les attributs qui vont bien :)
Fréquence de l'affichage = 60hz
Nombre de cycles=1 000 000 / 60 = 16667 cycles/frame
On perd donc 3333 cycles en 60hz. Il vaut donc mieux rester en 50hz !

Sachant que le NOP fait 2 cycles, on peut donc exécuter: 20 000/2=10 000 NOPs/frame
Un NOP, ca ne fait rien du tout. Ce qui pourrait être intéressant, c'est de savoir combien de temps ça prend pour effacer l'écran. En théorie, si on se moque de la place occupée, il suffit de compter le temps pour chaque STA:

  

LDA #' ' ; 2 cycles (2 octets) Un ESPACE pour effacer l'écran TEXT
STA $ECRAN ; 4 cycles (3 octets)
STA $ECRAN+1
STA $ECRAN+2
...
STA $ECRAN+1118
STA $ECRAN+1119

Cette routine prend exactement 2+3*1120=3362 octets en mémoire, et s'exécute en 2+4*1120=4482 cycles, soit 22% du temps d'une frame. On pourrait donc remplir 4,46 fois l'écran de cette façon.

Bon ok, c'est bourrin, je vous l'accorde. Et si on faisait ca avec une boucle ?

- NOTE:

Les données en cycle sont approximatives. En gros, il faut savoir que tous les branchements conditionnés (BNE,BCC,BEQ,...) prennent 3 cycles lorsque le branchement s'exécute, et 2 cycles sinon. Donc, vu qu'on fait des boucles, TOUS les branchements prennent 3 cycles, sauf le dernier qui n'en prend que 2. De même, si l'adresse d'arrivée n'est pas dans la même page, on rajoute encore un cycle.
Pour les indexations (sta $1234,X) on rajoute 1 cycle si le fait de rajouter X fait arriver dans une autre page. J'ai tout arrondi dans les calculs. C'est pour avoir une idée générale du temps occupé, on est pas à 5% près :)
On pourrait aussi mettre la routine en page zéro pour gagner du temps sur l'auto modification, ou bien utiliser un pointeur en page zéro, bref ca ne change pas grand chose au final sur le temps occupé :)

FIN DE NOTE -

;-------------- Init
LDA #LOW(ECRAN-1) ; 2
STA _patch_adr+1 ; 4
LDA #HIGH(ECRAN-1) ; 2
STA _patch_adr+1 ; 4

LDY #28 ;2 --> 2+4+2+4+2=14 cycles d'init

;-------------- Fin init

 

loop_y

  LDX #40 ;2
LDA #' ' ;2 -> 2+2=4 pour début boucle Y

;--------------- Boucle interne

loop_x

_patch_adr
STA $ECRAN,x ;4-5
DEX ;2
BNE loop_x ;2-3 --> (4+2+3)*40=360 cycles / ligne (grosso-modo)

;--------------- Fin boucle interne

CLC ;2
LDA _patch_adr+1 ;4
ADC #40 ;2
STA _patch_adr+1 ;4
LDA _patch_adr+2 ;4
ADC #0 ;2
STA _patch_adr+1 ;4
DEY ;2
BNE loop_y ;2-3 --> (2+4+2+4+4+2+4+2+3)=27 cycles par itération Y

--> Calcul final:

14 d'init +28 lignes * (360+27)
=14+28*387
=14+10836
=10850 cycles !!!!!

Soit 54% du temps d'une frame. Impressionnant non ? Et ce n'est que pour le mode TEXT :)
En hires, il y a presque 8 fois plus de données à effacer.

Résumons donc les avantages et inconvénients des deux méthodes:

- La méthode 1 est TRES rapide, mais occupe BEAUCOUP de mémoire.
- la méthode 2 est BEAUCOUP plus lente, mais est TRES économe en mémoire.

Je vous propose donc la méthode numéro 3:

L'idée est d'aligner tous les transferts sur des multiples de 256 pour éviter la pénalité de +1 pour être sorti de la page lors des STA $1234,x

ECRAN_ALLIGN=((ECRAN/256)*256)+256 ; Calcule la première adresse alignée

LDA #' ' ; 2

LDX #0 ; 2 (On met 0 pour faire 256 itérations :)

loop_x_main

STA $ECRAN_ALLIGN+256*0,x ; 4
STA $ECRAN_ALLIGN+256*1,x ; 4
STA $ECRAN_ALLIGN+256*2,x ; 4
DEX ; 2
BNE loop_x_main ; 2-3 --> (4+4+4+2+3)*256=4354
LDX #ECRAN_ALLIGN-ECRAN ; 2 (128 octets au début de l'écran)

loop_x_start

STA $ECRAN,x ; 4
DEX ; 2
BNE loop_x_start ; 2-3 --> (4+2+3)*128+2=1154
LDX #(ECRAN+1120)-ECRAN_ALLIGN ; 2 (224 octets à la fin)

loop_x_end

 

STA $ECRAN_ALLIGN+256*3,x ; 4
DEX ; 2
BNE loop_x_end ; 2-3 --> (4+2+3)*224+2=2018

Total:

2+4354+1154+2018=7528 cycles
Soit 37% du temps d'une frame.

On doit pouvoir encore améliorer ça en simplifiant !
Sachant que l'on se contente de remplir tout l'écran avec une seule valeur, et qu'une GROSSE partie du temps est perdue dans les DEX+BNE (5 cycles à chaque fois), on peut ruser en écrivant plusieurs fois le même pixel:

ECRAN_ALLIGN=((ECRAN/256)*256)+256 ; Calcule la première adresse alignée

LDA #' ' ; 2
LDX #0 ; 2

loop_x_main

STA $ECRAN+256*0,x ; 4-5
STA $ECRAN+256*1,x ; 4-5
STA $ECRAN+256*2,x ; 4-5
STA $ECRAN+256*3,x ; 4-5
STA $ECRAN+1120-224,x ; 4-5 (Dernière partie)
DEX ; 2
BNE loop_x_main ; 2-3

--> (4+4+4+4+4+2+3)*256=6400 dans le cas optimal
--> (5+5+5+5+5+2+3)*256=7680 dans le pire cas

Mais vu que l'écran commence en $BB80, c'est à dire à une moitié de page, on a 50% des itérations qui sont dans la page, et 50% en dehors, ce qui donne:

(5*4+2+3)*128+(5*5+2+3)*128=3200+3840=7040 cycles

ECRAN_ALLIGN=((ECRAN/256)*256)+256 ; Calcule la première adresse alignée

LDA #' ' ; 2

LDX #128 ; 2

loop_x_main

DEX ; 2
STA $ECRAN+128*0,x ; 4
STA $ECRAN+128*1,x ; 4
STA $ECRAN+128*2,x ; 4
STA $ECRAN+128*3,x ; 4
STA $ECRAN+128*4,x ; 4
STA $ECRAN+128*5,x ; 4
STA $ECRAN+128*6,x ; 4
STA $ECRAN+128*7,x ; 4
STA $ECRAN+128*8,x ; 4 (A vérifier pour celui-la, on finit en BFFF)
BNE loop_x_main

; 2-3

--> (2+2)+(2+4*8+3)*128=2+2+5760=5764 cycles
Soit 28% du temps d'une frame.

Le seul petit problème dans cette routine, c'est que de BFDF à BFFF, on à 32 octets qui sont signalés comme étant "forbiden". Il se trouve que l'Atmos ne plante pas quand on POKE à ces adresses, je pense donc que se sont 32 bons octets de VRAI ram qui se trouvent être placés à un endroit pas très pratique. A part ca, cette dernière routine à le mérite d'être relativement facile à comprendre, économe en place, et relativement rapide. Aucun cycle n'est perdu dans des sauts de pages et autres trucs dans ce genre.

J'espère que ceci vous aura aidé à comprendre l'intérêt du "code déroulé". On a souvent tendance à minimiser le temps occupé par les boucles et autre décrémentations.

Et en HIRES ???????

L'écran HIRES commence en $A000, donc aligné sur un multiple de 256. C'est sympa pour les routines. Il y a 200 lignes de 40 octets, soit 8000 octets de mémoire vidéo (hors texte).
En utilisant la méthode numéro 1, cela prendrait 2+4*8000=32002 cycles, soit 160% du temps d'une frame. Il n'est donc PAS POSSIBLE de remplir l'écran HIRES en une frame sur ORIC. Cela complique sérieusement les choses ! (sans compter les 24000 octets occupés par la routine !)
Cela explique donc pourquoi dans mes démos en haute résolution, je ne trace qu'une ligne sur 2. Mais il y a aussi une autre raison que j'expliquerai une autre fois.

 


Les articles du mois