Dots tunnel : deux écrans, et n'effacer que les points
Le deuxième article a laissé le tunnel sur le CPC dans une première version lente, et qui clignote. Sa mesure accusait l’effacement des 16 Ko de l’écran, alors qu’une image n’allume que 256 octets au plus.
Les deux défauts tombent ici l’un après l’autre. La v2 dessine dans un second écran, caché, et ne clignote plus. La v3 n’efface plus que les octets que le tunnel a allumés. Il restera à mesurer ce que rapporte chaque version, et ce que coûte encore une image quand l’effacement ne pèse presque plus rien.
Pour rappel, une trame du CPC dure 19 968 NOPs, et la v1 en prend 7 par image.
Pourquoi l’image clignote
La v1 dessine dans l’écran affiché, à #C000. Le faisceau balaie donc cet écran pendant que clear16k le vide, puis pendant que les points reviennent un par un. Il montre ainsi près de 5 trames d’un écran noir ou à moitié vide, puis 1 ou 2 trames d’un tunnel qui se reconstruit. L’œil voit les deux, et l’image clignote.
Le remède est connu de toutes les démos : dessiner dans un écran que personne ne regarde, et ne le montrer qu’une fois fini.
Deux écrans
Le CPC n’a pas d’écran fixe. Le CRTC, le circuit qui fabrique les adresses de la mémoire vidéo, lit l’adresse de départ de l’image dans ses registres 12 et 13. Les bits 5 et 4 de R12 choisissent le bloc de 16 Ko : 11 pour #C000, 10 pour #8000. Les bits 3 et 2 fixent la taille de l’écran, 16 Ko quand ils valent 00. Les bits 1 et 0 de R12, avec R13, donnent un décalage dans ce bloc, que le tunnel laisse à 0.
La mémoire a la place d’un second écran. Les tables vont de #4000 à #69FF, le code part de #1000, et le bloc #8000 à #BFFF est libre. Le snapshot de RASM le livre rempli de zéros, donc noir.
Choisir l’écran affiché
show_screen reçoit l’octet haut de l’écran, #C0 ou #80, et l’écrit dans R12. Le CRTC a deux ports : #BCxx choisit le registre, #BDxx y écrit :
; show_screen : le CRTC affichera l'écran de poids fort A (#C0 ou #80)
; à partir de la prochaine image.
show_screen:
ld bc,#BC0C ; CRTC : choisit le registre 12
out (c),c
rrca
rrca
and #30 ; #C0 -> #30, #80 -> #20
inc b ; #BD : écrit dans le registre
out (c),a
ret
Deux rrca descendent les bits 7 et 6 de l’octet haut aux places 5 et 4, et and #30 efface le reste. inc b passe de #BC à #BD sans toucher à C.
Le CRTC ne prend la nouvelle adresse qu’au début de l’image suivante, quand il recommence en haut de l’écran. Écrire R12 juste après le début de la VBL laisse donc l’image en cours se finir sur l’ancien écran. La suivante part du nouveau, en entier.
La boucle
back garde l’octet haut de l’écran de travail, celui où l’on dessine :
back: db #C0 ; poids fort de l'écran de travail
Chaque image commence par montrer l’écran que la boucle vient de finir, puis passe à l’autre. xor #40 fait l’échange, car #C0 et #80 ne diffèrent que par le bit 6 :
main: call wait_vbl
ld a,(back)
call show_screen ; affiche l'écran qu'on vient de dessiner
ld a,(back)
xor #40 ; #C0 <-> #80
ld (back),a ; et dessine dans l'autre
border RED ; début du travail
ld a,(back)
ld h,a ; l'écran de travail, caché
ld l,0
call clear16k
Au premier tour, back vaut #C0 : la boucle montre l’écran #C000, encore noir, et dessine dans #8000. Au tour suivant, elle montre #8000 et dessine dans #C000, et ainsi de suite.
Il faut relire back après border RED, car la macro border charge sa couleur dans A. Sans cette relecture, clear16k effacerait 16 Ko à partir de #4C00, au beau milieu des tables. Le tunnel se déferait dès la première image. Les anneaux proches laisseraient des cercles pleins, car l’écran de travail ne serait plus jamais effacé, et les autres, dont la forme serait remise à zéro, se réduiraient à leur centre.
Un point
plot ajoutait SCREEN>>8, une constante, à l’octet haut de l’adresse. Il ajoute maintenant back, qui change à chaque image :
inc h ; YHI = YLO + 256
ld a,(back)
adc a,(hl) ; + poids fort, retenue et début de l'écran
ld a,(back) coûte 4 NOPs au lieu de 2 pour ld a,#C0. Sur 256 points, cela fait 512 NOPs par image, une broutille à côté de l’effacement.
Le résultat
Assemblé, tunnel-v2-fr.asm donne tunnel-v2.sna. Lancé dans un émulateur, ce snapshot montre un tunnel qui ne clignote plus. Chaque image affichée est complète, et l’écran dessiné est identique, octet pour octet, à celui de la v1 pour la même valeur de t. On l’a vérifié dans un émulateur Z80, en comparant les 16 Ko de l’écran de travail des deux versions pour les 256 valeurs de t.
La vitesse n’a pas bougé, avec 7 trames par image, car la boucle efface toujours 16 Ko, dans un écran que personne ne voit.
N’effacer que ce qu’on a dessiné
Le tunnel allume 256 points par image, donc 256 octets au plus. Deux points voisins peuvent tomber dans le même octet, et une image en allume donc souvent moins. Pour effacer une image, il suffit de remettre ces octets à 0, à condition de savoir où ils sont.
plot connaît chaque adresse au moment où il l’écrit. Il peut donc la noter dans une liste, et la boucle de l’image suivante relira cette liste pour effacer.
Une liste par écran
Avec deux écrans, l’image qu’on efface n’est pas la précédente. L’écran de travail contient l’image dessinée deux tours plus tôt, puisque entre-temps la boucle a dessiné dans l’autre. Il faut donc une liste par écran, et la liste change en même temps que l’écran.
Chaque liste tient 256 adresses de 2 octets, soit 512 octets. Le bloc #6A00 à #7FFF, entre les tables et le second écran, est libre, et les deux listes s’y alignent sur 256 :
LIST_C0 = #7C00 ; les octets allumés dans l'écran #C000
LIST_80 = #7E00 ; et dans l'écran #8000
erase_list garde l’octet haut de la liste de l’écran de travail :
erase_list: db LIST_C0>>8 ; poids fort de sa liste
Des listes prêtes dès la première image
Au premier passage sur un écran, sa liste n’a encore jamais été écrite. Elle ne contient que les zéros du snapshot, et l’effacement écrirait 256 fois à l’adresse #0000, hors de l’écran. fill_list remplit donc chaque liste avec l’adresse du premier octet de son écran :
; fill_list : remplit la liste HL de 256 fois l'adresse A * 256, le premier
; octet d'un écran encore noir : le premier effacement n'y change rien.
fill_list:
ld b,0
next_entry:
ld (hl),0
inc hl
ld (hl),a
inc hl
djnz next_entry
ret
ld b,0 fait faire 256 tours à djnz, car le premier djnz fait passer B de 0 à 255. Le premier effacement écrit alors 256 fois 0 dans un octet qui vaut déjà 0.
Le programme appelle fill_list une fois par liste, juste après init :
call init
ld hl,LIST_C0
ld a,#C0
call fill_list
ld hl,LIST_80
ld a,#80
call fill_list
Effacer en suivant la liste
La boucle bascule la liste avec l’écran. #7C et #7E ne diffèrent que par le bit 1, et xor #02 fait l’échange :
ld a,(erase_list)
xor #02 ; #7C <-> #7E : la liste suit l'écran
ld (erase_list),a
L’effacement remplace ensuite clear16k. IY parcourt la liste, deux octets par adresse, et chaque adresse reçoit un 0 :
ld a,(erase_list) ; efface les octets allumés deux images plus tôt
ld h,a
ld l,0
push hl
push hl
pop iy ; IY parcourt la liste
ld b,0 ; 256 adresses
erase: ld l,(iy+0)
ld h,(iy+1)
ld (hl),0
inc iy
inc iy
djnz erase
pop iy ; plot réécrit la liste depuis le début
Le double push hl garde deux copies du début de la liste sur la pile. La première passe dans IY pour l’effacement. La seconde remet IY au début une fois l’effacement fini, pour que plot réécrive la liste par-dessus l’ancienne.
Un octet qui porte deux points figure deux fois dans la liste, et reçoit deux fois 0. Le second effacement ne coûte que du temps, et ne détruit rien : à ce moment, aucun point de la nouvelle image n’est encore dessiné.
Noter chaque adresse
plot note l’adresse qu’il vient d’écrire, juste avant son ret :
or (hl) ; garde les autres pixels de l'octet
ld (hl),a
ld (iy+0),l ; note l'adresse pour l'effacement
ld (iy+1),h
inc iy
inc iy
ret
Rien d’autre dans le programme ne touche à IY, qui peut rester réservé à la liste pendant toute l’image.
Le tunnel sur le CPC
L’émulateur CPCEC ci-dessous lance tunnel-v3.sna, assemblé à partir de tunnel-v3-fr.asm :
L’image ne clignote pas, et le tunnel va nettement plus vite. La bordure rouge déborde encore sur une deuxième trame. Là aussi, l’écran dessiné est identique, octet pour octet, à celui de la v1 pour la même valeur de t.
Ce que rapporte chaque version
On a mesuré les trois versions comme la v1 dans le deuxième article, en trames par image dans amspirit-lite (6128), sur 1 000 trames. La mesure passe cette fois par sa version sans fenêtre, amspirit-lite-headless :
| Version | Trames par image | Images par seconde |
|---|---|---|
v1 : un écran, clear16k |
7 | 7 |
v2 : deux écrans, clear16k |
7 | 7 |
| v3 : deux écrans, liste d’effacement | 2 | 25 |
Le compte à la main explique l’écart. ldir efface 16 Ko en 98 297 NOPs. Un tour de la boucle erase coûte 23 NOPs :
| Instruction | NOPs |
|---|---|
ld l,(iy+0) |
5 |
ld h,(iy+1) |
5 |
ld (hl),0 |
3 |
inc iy |
3 |
inc iy |
3 |
djnz erase |
4 (3 au dernier tour) |
Les 256 tours font 255 × 23 + 22 = 5 887 NOPs, soit 17 fois moins que clear16k. En échange, plot paie 16 NOPs de plus par point pour noter l’adresse (5 + 5 + 3 + 3), soit 4 096 NOPs par image.
Ces 16 NOPs expliquent aussi pourquoi la v3 ne tient pas dans une trame. Le deuxième article comptait déjà plus de 78 NOPs par point. Avec les 2 NOPs de ld a,(back) et les 16 de la liste, il en faut maintenant plus de 96. Les points seuls coûtent donc plus de 256 × 96 = 24 576 NOPs, au-delà des 19 968 NOPs d’une trame, avant même l’effacement. Effacer moins ne servira plus à rien.
Conclusion
La v3 ne clignote plus et tourne à 25 images par seconde, contre 7 pour la v1, avec les mêmes images, octet pour octet. Elle garde pourtant les défauts qui suivent :
- 2 trames par image, donc la moitié des 50 images par seconde du CPC ;
- plus de 96 NOPs par point, entre le
call, les variables en mémoire, la boucle du masque et maintenant la liste ; - 1 Ko de listes et un second écran de 16 Ko, la mémoire que coûte la vitesse.
Le prochain article s’attaquera au coût de chaque point.
Les fichiers de l’article :
- tunnel-v2-fr.asm et tunnel-v2.sna, la v2 avec deux écrans ;
- tunnel-v3-fr.asm et tunnel-v3.sna, la v3 avec sa liste d’effacement ;
- TABLES.BIN, les tables du BASIC, que les deux versions chargent.
Les deux versions s’assemblent avec RASM 3.0.9.