Blog

Dots tunnel : un écran de 256 × 256 et 400 points

31/10/2026effets de démodots tunnelCPCZ80

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 :

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 :

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 :

Les deux versions s’assemblent avec RASM 3.0.9, sans fichier de tables.