Dots tunnel : un écran de 256 × 256 et 400 points
Le quatrième article a fait tenir le tunnel dans une trame, avec un point à 43 NOPs. Dans ce point, le passage de y à l’adresse reste le plus cher, 19 NOPs sur 43, entre deux lectures de table et une addition avec retenue.
Cette retenue vient de la largeur de l’écran : une ligne du CPC fait 80 octets, et c’est ce nombre qui oblige à additionner. Aucune instruction n’y peut rien, mais la forme de l’écran se règle. La v6 programme donc le CRTC pour un écran de 256 × 256 pixels, où l’addition devient un or, sans retenue. La v7 dépense ensuite le temps gagné en points, jusqu’à presque remplir la trame.
Ce que coûte une ligne de 80 octets
Dans l’écran standard, la ligne y commence à 80*(y>>3) + 2048*(y and 7) octets du début de l’écran.
L’octet bas de 80*(y>>3) va de 0 à 240 selon la rangée. Ajouter la colonne du point (x >> 2) peut donc déborder sur l’octet haut, et la v5 doit enchaîner add a,c puis adc a,(hl). Comme l’octet haut dépend de cette retenue, la v5 ne peut pas le lire directement dans une table.
Avec 64 octets par ligne, la ligne y commence à 64*(y>>3) + 2048*(y and 7). L’octet bas de 64*(y>>3) est toujours un multiple de 64 : ses 6 bits bas valent 0. Une colonne va de 0 à 63 et remplit exactement ces 6 bits. L’addition devient un or, qui ne déborde jamais, et l’octet haut ne dépend plus que de y.
Une ligne de 64 octets compte 256 pixels en mode 1, et x tient justement dans un octet. Avec 256 lignes, y aussi tient dans un octet, et 64 × 256 = 16 384 : l’écran remplit exactement sa page de 16 Ko. Les deux écrans restent donc en #C000 et #8000.
Le réglage du CRTC
Le CRTC, le circuit qui produit les adresses de la mémoire vidéo et les synchronisations du moniteur, se règle par ses registres, dont quatre changent ici :
- R1, la largeur affichée, passe de 40 à 32 caractères, soit 64 octets par ligne ;
- R6, la hauteur affichée, passe de 25 à 32 rangées de 8 lignes, soit 256 lignes ;
- R2 et R7 placent les synchronisations horizontale et verticale. Ils passent de 46 à 42 et de 30 à 34, pour que l’image reste au milieu du moniteur.
init écrit ces quatre registres à la fin, après la palette. Une paire de ports suffit : #BC choisit le registre, #BD y écrit la valeur :
ld hl,crtc_256
ld e,4
next_reg:
ld b,#BC ; CRTC : choisit le registre
ld a,(hl)
out (c),a
inc hl
inc b ; #BD : écrit dans le registre
ld a,(hl)
out (c),a
inc hl
dec e
jr nz,next_reg
La table placée après le code donne, pour chaque registre, son numéro puis sa valeur :
; R1 = 32 mots (64 octets) par ligne, R6 = 32 rangées de 8 lignes ; R2 et R7
; déplacent les synchros pour garder l'image au milieu du moniteur.
crtc_256:
db 1,32, 2,42, 6,32, 7,34
show_screen ne change pas : R12 désigne toujours l’écran affiché, #C000 ou #8000.
Des tables calculées par RASM
TABLES.BIN ne convient plus. Ses tables YLO et YHI suivent les lignes de 80 octets, et CENTER place le tunnel en (160, 100), le centre de l’écran standard. Le deuxième article a montré que RASM calcule les mêmes tables que le BASIC, octet pour octet. La v6 lui confie donc toutes les tables, sauf XMASK et COLX, que make_x_tables construit toujours au démarrage. Le .sna contient alors tout, sans le fichier du BASIC.
Le tunnel garde ses paramètres et se recentre en (128, 128). Ses points vont alors de 38 à 218 sur les deux axes, et restent tous dans l’écran.
Puisque l’octet haut ne dépend que de y, YHI peut le donner complet, début de l’écran compris. Chaque écran a alors sa paire de tables, et voici celle de l’écran #C000 :
; Une ligne fait 64 octets : la ligne y commence en 64*(y>>3) + 2048*(y and 7).
; Son octet bas est un multiple de 64, et une colonne d'octet (0 à 63)
; remplit ses 6 bits bas.
org YLO_C0
repeat 256,y,0
db ((y>>3)*64)&255
rend
repeat 256,y,0 ; YHI_C0 = YLO_C0 + 256
db SCREEN_C0+(((y>>3)*64+(y&7)*2048)>>8)
rend
La paire de l’écran #8000, en #6A00, ne diffère que par SCREEN_80.
Un point à 39 NOPs
Le point de la v6 lit l’octet bas de son adresse dans YLO, puis l’octet haut dans YHI, à la même position y :
repeat DOTS,n,0 ; les 16 points, l'un après l'autre
ld a,(de) ; dx
inc e ; les 32 octets d'un anneau restent dans une page
add a,ixl ; x = centre x de l'anneau + dx
ld l,a
ld h,XMASK>>8
ld a,(hl) ; bits du pixel dans son octet
and b ; dans l'encre
ex af,af' ; mis de côté pour la fin
inc h ; COLX = XMASK + 256
ld c,(hl) ; colonne d'octet : x >> 2
ld a,(de) ; dy
inc e
add a,ixh ; y = centre y de l'anneau + dy
ld l,a
ypatch{n}:
ld h,0 ; page YLO de l'écran de travail, écrite par main
ld a,(hl)
or c ; poids faible de la ligne + colonne : sans retenue
inc h ; YHI = YLO + 256
ld h,(hl) ; poids fort, début de l'écran compris
ld l,a
ex af,af' ; de nouveau les bits du pixel
or (hl) ; garde les autres pixels de l'octet
ld (hl),a
push hl ; note l'adresse pour l'effacement
rend
Après ld a,(hl), L contient encore y. inc h passe de YLO à YHI, et ld h,(hl) range l’octet haut directement dans H. La v5 devait garder l’octet bas dans C, lire le début de l’écran dans IYH, puis additionner avec la retenue. Ici, passer de y à l’adresse coûte 7 NOPs au lieu de 11. Le or lui-même coûte 1 NOP, autant que l’add qu’il remplace, et les 4 NOPs gagnés viennent tous de l’octet haut, que la table donne tel quel.
Du code qui se réécrit
Il reste à indiquer au point la paire de tables de l’écran de travail, qui change à chaque image. Le point la trouve dans l’opérande de ld h,0, que main réécrit avant de dessiner. C’est du code auto-modifié : le programme change un octet de ses propres instructions.
RASM numérote les 16 copies du point : dans repeat DOTS,n,0, l’étiquette ypatch{n} devient ypatch0, ypatch1, jusqu’à ypatch15. L’étiquette désigne le code de l’instruction, et son opérande est à l’octet suivant, d’où le +1. À chaque image, main change de page en même temps que d’écran, puis écrit la nouvelle page dans les 16 copies :
ld a,(ypage)
xor (YLO_C0>>8)^(YLO_80>>8)
ld (ypage),a ; la table YLO de l'écran de travail
repeat DOTS,n,0
ld (ypatch{n}+1),a ; va dans le ld h de chaque point
rend
Ces 16 écritures coûtent 64 NOPs par image. La v5 payait 2 NOPs par point pour lire IYH, soit 512 par image, et IYH ne sert plus.
Le point tient en 39 NOPs :
| Étape | v5 | v6 |
|---|---|---|
| x : bits du pixel et colonne | 15 | 15 |
| y | 8 | 8 |
| adresse | 11 | 7 |
| écriture et liste | 9 | 9 |
| total | 43 | 39 |
Le point gagne 4 NOPs, soit 1 024 par image. Le travail de l’image passe d’environ 14 000 à environ 13 000 NOPs : 1 280 pour l’effacement, 256 × 39 = 9 984 pour les points, 1 656 pour préparer les 16 anneaux.
La v6 dessine le même tunnel que la v1. On l’a vérifié dans un émulateur Z80 : pour les 256 valeurs de t, les points allumés de la v6 sont ceux de la v1, décalés de 32 pixels vers la gauche et de 28 vers le bas.
400 points
La v6 laisse environ 6 800 NOPs libres dans la trame, et la v7 les dépense en points.
Ajouter des anneaux ne marche pas, comme l’a montré le quatrième article : 16 anneaux espacés de 4 profondeurs couvrent déjà les 64, et un dix-septième retomberait sur le premier. La v7 ajoute donc des points à chaque anneau, 25 au lieu de 16, soit 400 en tout.
La forme d’un anneau
Le point k d’un anneau reste à l’angle 4k + rot, mais un tour compte maintenant 4 × 25 = 100 pas au lieu de 64. Quatre phases de rotation suffisent toujours : après 4 pas, chaque point a pris la place de son voisin.
Une rangée (d, rot) de SHAPE contient maintenant 50 octets. La v7 lui réserve 64 octets. Une rangée ne déborde ainsi jamais de sa page de 256 octets, et inc e suffit pour avancer. defs complète chaque rangée par 14 octets à 0 :
org SHAPE
repeat 64,d,0
s = P*R/(Z0+d*DZ)
repeat 4,rot,0
repeat DOTS,k,0
angle = 360*(4*k+rot)/(4*DOTS)
db floor(s*cos(angle)+0.5)&255
db floor(s*sin(angle)+0.5)&255
rend
defs ROW-2*DOTS,0 ; une rangée reste dans sa page : inc e suffit
rend
rend
Les quatre rangées d’une profondeur font 256 octets, une page entière. L’adresse d’une rangée devient SHAPE + 256 × d + 64 × rot : D reçoit l’octet haut de SHAPE plus d, et E reçoit rot × 64. La v6, comme la v5, testait encore si d était impair. La v7 calcule l’adresse de la rangée sans ce test :
ld a,(t) ; DE = SHAPE + d * 256 + rot * 64
and #0C
rlca
rlca
rlca
rlca ; rot * 64, rot = (t >> 2) and 3
ld e,a
ld a,c
add a,SHAPE>>8
ld d,a
La préparation d’un anneau descend ainsi de 103,5 à 99 NOPs : le test coûtait 5,5 NOPs en moyenne, et le quatrième rlca n’en coûte qu’un.
La mémoire
SHAPE passe à 16 Ko, 64 profondeurs de 256 octets. Elle prend toute la zone de #4000 à #7FFF, juste sous l’écran #8000. Les autres tables descendent entre #2000 et #2DFF, au-dessus du code, qui finit vers #1770. Les deux listes d’effacement gardent maintenant 400 adresses, 800 octets chacune, en #3000 et #3400.
Pour le reste, changer DOTS suffit : repeat RINGS*DOTS déroule 400 effacements, repeat DOTS,n,0 recopie 25 fois le point, et main écrit 25 opérandes au lieu de 16.
Le budget
Le compte à la main donne, par image :
| Poste | Calcul | NOPs |
|---|---|---|
| effacement | 400 × 5 | 2 000 |
| points | 400 × 39 | 15 600 |
| préparation des anneaux | 16 × 99 | 1 584 |
| total du travail | environ 19 200 |
Avec la boucle main et les 25 opérandes à réécrire, l’image prend environ 19 450 NOPs, 97 % d’une trame. Il reste environ 500 NOPs. Un 26e point par anneau coûterait 16 × 44 = 704 NOPs (39 pour le dessiner, 5 pour l’effacer) et ne tiendrait plus.
La v7 dessine bien ce que ses tables décrivent : pour les 256 valeurs de t, l’émulateur Z80 retrouve exactement les points que CENTER et SHAPE prévoient.
Le tunnel sur le CPC
L’émulateur CPCEC ci-dessous lance tunnel-v7.sna, assemblé à partir de tunnel-v7-fr.asm :
L’écran est maintenant carré, avec une bordure à gauche et à droite.
Ce que rapporte chaque version
On a mesuré les trois versions comme dans les articles précédents, en trames par image dans amspirit-lite-headless (6128), sur 1 000 trames :
| Version | Points | Trames par image | Images par seconde |
|---|---|---|---|
| v5 : écran standard | 256 | 1 | 50 |
| v6 : écran de 256 × 256 | 256 | 1 | 50 |
| v7 : 25 points par anneau | 400 | 1 | 50 |
Conclusion
L’écran de 256 × 256 ramène le point à 39 NOPs, et la v7 affiche 400 points à 50 images par seconde, 144 de plus que la v5, avec ces limites :
- la trame est presque pleine : il reste environ 500 NOPs par image ;
- l’écran n’est plus celui du système : le BASIC et le firmware ne savent pas y écrire, et les deux écrans occupent toute la mémoire au-dessus de
#8000; - la mémoire sous les écrans est presque pleine aussi, avec 16 Ko pour la seule table
SHAPE; - le code fait 1 900 octets, dont 700 pour les 25 copies du point et 800 pour l’effacement déroulé.
L’article suivant change de méthode : au lieu d’une routine commune à tous les points, il produit du code compilé pour ce tunnel-là.
Les fichiers de l’article :
- tunnel-v6-fr.asm et tunnel-v6.sna, la v6 sur l’écran de 256 × 256 ;
- tunnel-v7-fr.asm et tunnel-v7.sna, la v7 à 400 points.
Les deux versions s’assemblent avec RASM 3.0.9, sans fichier de tables.