ASMtrad CPC

Apprenez l'assembleur Z80

Eviter le balayage.

Parmis les effets non voulus lors de l'affichage nous avons ce que nous appelons "se manger le balayage".
Concrètement c'est la conséquence de deux choses ou de l'une des deux:

- Une synchronisation avec le balayage non maitrisée.
- Une routine d'affichage trop lente.

Se synchroniser avec le balayage du moniteur de façon précise n'est pas une obligation et nous vérrons dans un autre cour que l'on peut presque totalement s'en passer

Cependant, dans le cas ou l'on n'utilise qu'un seul écran en RAM, se synchroniser de façon précise devient presque obligatoire

Nous allons donc parler un peu des interruptions, et ce pour plusieurs raisons:

-Elles vont nous permettre, de nous caler pour faire des rasters à des endroits "précis" de l'écran.
-Elles vont nous permettre, de nous caler pour changer de mode à des endroits "précis" de l'écran.
-Elles vont nous permettre, d'éviter de se prendre le balayage en plein milieu d'un sprite.
-Elles nous permettront aussi de faire tourner la musique toute seule comme une grande par exemple.

Et ce ne sont que des exemples car la possibilité d'utilisation est quasi infinie.

Nous allons surtout nous interesser dans ce cours au fait de se manger le balayage lors de l'affichage d'un sprite.
Ceci est surtout visible lorsque vous le déplacez et que étrangement, le haut et le bas se retrouvent décalés.
Se manger le balayage c'est ça...

En effet déplacer un sprite c'est bien, mais il est possible que entre le temps ou vous commencez à afficher votre héro et le temps où votre routine termine de l'afficher, le balayage de votre moniteur soit déjà arrivé plus loin.

Votre sprite apparait donc comme décalé horizontalement (si vous le déplacez horizontalement).
C'est moche.

Ou bien celui-ci n'est pas affiché à temps et entre la restitution du fond et l'affichage de sprite, ça clignote.

Afin de mieux comprendre le phénomène, voici un exemple plus visuel:

se_manger_le_balayage1se_manger_le_balayage2

A cela plusieurs solutions. L'une d'elle dont nous parlerons plus tard consiste à travailler sur deux écrans.
Pendant que l'un est visible, vous affichez sur l'autre.
Une fois fini d'afficher, vous basculez vers l'autre écran et hop on n'y voit que du feu.

C'est une excellente technique mais qui vous coute le double de RAM que l'écran fait de taille.

Ce que nous allons faire ici, c'est d'essayer tant que possible de rester à 50Hz sans se manger le balayage.

Pour cela, nous allons choisir quand nos différentes routines seront exécutées, afin justement de finir l'affichage avant que le balayage ne commence à balayer la zone sur le moniteur.

Mais cela va imposer plusieurs choses:

- Connaitre le temps pris par nos routines afin de ne pas dépasser le temps alloué.
- Maitriser les interruptions pour se caler dessus.

Nous verrons deux façons de faire.

La première consistera juste à faire des HALT pour se caler dessus.
La deuxième consistera à placer nos évènements sous interruption.

Enfin, quand nous aurons vu cela, nous verrons justement comment travailler sur deux écrans ce qui nous permettra entre autre d'avoir des jeux tournants à moins de 50Hz.

Les interruptions.

Nous n'allons parler pour le moment que du mode IM1, mode par défaut du Z80A.
A savoir qu'il existe aussi l'IM2 et dans une moindre mesure l'IM0.
IM signifiant "Interruption Mode".

En IM 1, le Z80A provoquera (ce n'est pas tout a fait le cas, mais on verra cela une autre fois puisque les ints sont générées par le GA) 300 fois par seconde une interruption.
300 fois par seconde cela nous fait 6 fois par écran.
Soit tous les 6.5 blocs de lignes environ.

Pour vous donner une idée, sur l'image suivante, la couleur du border est changée à chaque interruption.
Pour abréger je dirais désormais INT.

int

À chaque INT, notre Z80A saute directement en #38.
Peu importe que vous soyez en train d'afficher un sprite; jouer une musique ou autre: il saute en #38.

L'adresse courante avant le saut (donc contenue dans le registre interne PC: Program Counter.) est sauvegardée dans la pile et le Z80A passe en DI (DI=Disable Interruptions=couper les interruptions).

En temps normal, en #38 on trouve un JP vers une adresse du system.
Là le cpc gère tout un tas de chose comme les couleurs ou le clavier par exemple.

Nous cela ne nous arrange pas pour la simple raison que cela lui prend du temps et par conséquent ca nous en vole aussi...

La solution c'est d'enlever ce JP en le remplaçant par EI:RET

Pourquoi pas juste RET ? Parce qu'on veut quand même rétablir les INT pour la prochaine fois.
Ainsi quand l'INT aura lieue, le Z80 sautera en #0038 exécutera EI:RET et retournera d’où il venait.

L’intérêt de garder les int actives, c'est qu'on peut se caler dessus avec l'instruction HALT.
HALT=Attend une int. Le Z80 enchaine les NOPs jusqu'à detecter le signal d'une INT.

Ajoutez donc dès le début de votre code:

                KILL_SYSTEM     DI    
                                LD      HL,#C9FB
                                LD      (#38),HL
                                EI

Pourquoi envoie-t'on #C9FB en #38 ?
#FB est l'opcode de l'instruction EI.
#C9 est l'opcode de l'instruction RET.
Lors d'un envoie d'une valeur 16 bits en RAM, les octets sont inversés.
#FB est donc envoyé en #38.
#C9 est donc envoyé en #39.

Comme vous le voyez sur l'écran au dessus, on peut donc se caler à 6 endroits (le premier étant tout en haut pendant la VBL).
Et comme on est calé on peut aussi décider à un des HALT de changer les couleurs; changer de mode ou autre...

Placer judicieusement les routines d'affichage.

Cette façon de faire peu conformiste (en vérité elle n'est quasiment jamais utilisée alors qu'elle est pourtant facile à mettre en place.) ne fonctionne que sous certaines conditions:

- Vos routines d'affichage doivent être le plus rapide possible et passer sous la durée entre deux HALT si possible.
- L'ensemble de vos routines d'affichage ne doit pas dépasser la frame si vous ne voulez pas avoir à vous embêter à en plus gérer un numéro de frame.

Vous avez donc tout intérêt à optimiser (faire des routines plus rapides) au maximum vos routines si vous voulez utiliser cette technique (cela ne vous interdit pas de le faire avec une autre méthode.).

Pour mesurer le temps de votre routine rien de tel qu'un bon raster (changement de couleur pendant le balayage... on voit cela très bientôt).
Ajoutez donc en début de routine:

                                LD      BC,#7F10
                                OUT     (C),C
                                LD      C,76
                                OUT     (C),C

Et en fin de routine:

                                LD      BC,#7F10
                                OUT     (C),C
                                LD      C,84
                                OUT     (C),C

Vous verrez visuellement le temps pris par votre routine sans le border qui sera rouge.

Petit exemple avec un écran de jeu dont j'ai un peu optimisé son affichage de sprite.

optimisation

A gauche avant optimisation. A droite après... Comme quoi on peut gagner beaucoup.

Pour commencer, on va numéroter nos zones d'int (sachant que l'int est au début de chaque changement de couleur).

int_numero

Petite parenthèse: L'int ne dure pas tout le temps représenté par une couleur. Chaque couleur représente le temps entre deux int.
L'int en elle même dure quelques NOPs (4) puisque nous avons placé un EI:RET en #38.

Notre routine de sprite optimisée fait donc maintenant moins d'1 Halt (par abus de langage temps pris entre deux int: donc sur le schéma une couleur entière).

C'est parfait, il suffit donc de la mettre en dehors de la zone de jeu !!! Donc soit au 1er HALT; Soit au 6...
Mais attention, il n'y a pas que l'affichage du sprite... Il y a aussi la restitution du fond !!!

Si le sprite est affiché en 1 mais que la restitution du fond a lieue en 2 alors vous ne verrez tout simplement jamais votre sprite à partir du moment ou il bouge ...
A vous de réfléchir à la chose ;)
L'ordre est ici important.

Voyons un peu ce à quoi ressemblera le code:

                BOUCLE          CALL    FRAME

                                CALL    TEMPO
                                HALT                        ;INT n°2
                                HALT                        ;INT n°3
                                HALT                        ;INT n°4
                                HALT                        ;INT n°5
                                HALT                        ;INT n°6

                                CALL    TEST_DE_TOUCHE

                                JP      BOUCLE

Ceci est en gros le squelette de notre programme.
Entre chaque HALT on pourra placer nos routines.
Celles-ci ne devront pas dépasser l'espace entre deux HALT (ou il faudra en retirer).
Réfléchissez bien à ce que vous voyez. Si quelque chose est affiché pendant l'int 1 vous le verrez pendant les int 2,3,4,5 et le début du 6

J'ai mis "CALL TEMPO" juste après le test de FRAME.
La raison est simple: la première int ayant lieue en même temps que la VBL, vous risquez de la louper.
Plutôt que de prendre le risque, autant en être certain et la louper à chaque fois.
La tempo consiste juste à ajouter après le test VBL de quoi être certain de passer la première int.

TEMPO peu par exemple contenir:

                TEMPO           LD      B,32
                                DJNZ    $
                                RET

Notez le $. Ceci signifie "adresse actuelle". En gros ca saute sur place.

Maintenant à vous d'ajouter vos routines entre les halt ;)