30.10.26
Intro démo ZX Spectrum : créer un cracktro en Z8013 min de lecture2 672 mots

Intro démo ZX Spectrum : créer un cracktro en Z80

Découvrez comment créer une intro démo pour ZX Spectrum 128K : assembleur Z80, défilement, musique AY et étoiles, avec méthodes et limites concrètes.

intro démo ZX Spectrumprogrammation Z80démoscène rétroassembleur ZX Spectrummusique AY
Intro démo ZX Spectrum : créer un cracktro en Z80

Du rêve d'adolescent au cracktro : créer une intro démo sur ZX Spectrum

À la fin des années 1980, je passais des heures devant mon ZX Spectrum. Je recopiais des programmes en Sinclair BASIC et je piochais des routines en assembleur dans des magazines. Je voulais reproduire ces intros impressionnantes que j'avais vues chez un ami sur Amiga. Spoiler : sur Spectrum, ça ne rendait pas pareil. Il m'a fallu attendre plusieurs décennies pour finalement finir une intro démo maison, pensée pour tourner sur un ZX Spectrum 128K standard.

Le projet tient dans un fichier TAP, mais il y a quand même du boulot derrière : un défilement fluide, une musique trois voix sur puce AY, des vu-mètres et un champ d'étoiles en parallaxe. Le code fait 10 287 octets ; l'image cassette complète, 19 552. Ce qui m'intéresse surtout, ce n'est pas la taille en elle-même, c'est la méthode : comprendre une machine en remontant jusqu'à ses registres, ses contraintes d'affichage et le budget de calcul.

Voici le parcours, les choix techniques et les compromis qui m'ont mené de l'idée à une version terminée. Si vous voulez d'abord voir le résultat, la page du projet et son fichier TAP donnent accès à la démo.

Pourquoi revenir à la démoscène

Les intros de jeux piratés, les cracktros, étaient des petits écrans affichés avant le lancement d'un jeu. Elles servaient à signer un groupe, à saluer des gens et à montrer qu'on savait faire. On y voyait souvent un logo, un texte qui défile, de la musique, et un effet graphique : étoiles en parallaxe, objet en rotation, barres animées.

Ces écrans m'avaient vraiment marqué. Sur Amiga, la musique et les effets avaient un « air de magie » quand on venait d'un 8 bits sur cassette. La démoscène a ensuite élargi le concept : codeurs, graphistes et musiciens font des productions autonomes, conçues comme des démonstrations artistiques et techniques. L'idée reste la même, même sur des machines anciennes : chaque effet doit compter avec ce que le matériel permet réellement.

Mon projet était volontairement plus simple : une intro dans l'esprit des anciennes BBStros, avec un logo, un message défilant, une musique et un champ d'étoiles. Et il y avait aussi un côté pratique. Je travaillais déjà sur un portail TNFS pour Spectrum, un service en ligne accessible depuis la machine, et une intro dédiée m'a semblé être une façon sympa de le présenter.

Le défi n'était pas uniquement de (re)prendre le Z80. Après des années à bosser avec des langages plus « haut niveau », il faut réapprendre à raisonner en octets, en registres et en indicateurs d'état. Une animation qui paraît simple à l'écran demande en réalité une organisation précise des données et un contrôle très serré du temps d'exécution. La différence avec un projet moderne, c'est que vous ne choisissez pas juste une bibliothèque graphique : vous construisez les opérations nécessaires avec ce que la machine autorise.

Un environnement de développement simple

Je n'ai pas cherché une usine à gaz. J'ai fait au plus pragmatique : un Makefile, l'assembleur Pasmo, une version modifiée de zmakebas pour construire les fichiers BASIC, et l'émulateur FUSE pour les essais. Pour tester sur la vraie machine, j'envoyais les fichiers TAP vers mon serveur TNFS, puis je les lançais sur un Spectrum +3.

L'idée est simple : l'émulateur accélère les itérations et permet de corriger vite les erreurs. Mais le matériel réel reste nécessaire pour vérifier que ça se comporte comme prévu sur la machine ciblée. Les deux se complètent : l'un ne remplace pas l'autre.

Commencer par un écran et une boucle stable

J'ai d'abord vérifié la chaîne avec un programme minimal, puis j'ai affiché un fond préparé à l'avance. Sur Spectrum, un fichier SCREEN$ contient 6 912 octets : 6 144 pour les pixels et 768 pour les attributs de couleur. Ces données correspondent à la mémoire écran et peuvent être copiées à l'adresse attendue avec une instruction de transfert de bloc.

Une fois le fond chargé, le programme attend chaque interruption verticale. Le Spectrum standard tourne à 50 images par seconde : attendre cette interruption aide à synchroniser les mises à jour et évite les effets trop « rapides » ou trop « lents » selon les circonstances.

; Attendre chaque image

boucle:

halt

jr boucle

Ça semble presque trop basique, mais c'est exactement ce qu'il faut pour ajouter les éléments un par un. Je commençais par le logo, puis un premier message, puis les effets. Chaque ajout pouvait être testé sans déboguer toute la démo en même temps.

Utiliser la ROM sans en dépendre

Les routines de la ROM Spectrum peuvent afficher du texte et faire des tâches courantes. Les étudier aide à comprendre comment les paramètres passent par les registres et comment certaines boucles sont écrites. Mais les utiliser partout n'a pas forcément de sens : une routine « générale » peut imposer des dépendances ou faire du travail inutile pour votre intro.

Pour le texte d'essai, une routine de la ROM suffisait. Pour le défilement final, j'avais besoin de lire directement les données de la police et de manipuler les pixels. J'ai aussi remplacé la police intégrée en modifiant le pointeur utilisé par la ROM. C'est un compromis classique : réutiliser ce qui existe accélère le démarrage, mais un effet spécifique finit souvent par exiger un contrôle plus direct.

Pour comprendre le comportement des routines, la documentation désassemblée de la ROM Spectrum m'a beaucoup servi. Et un éditeur d'assembleur interactif aide à suivre les registres instruction par instruction. Mais honnêtement, dans ce projet, j'ai souvent gagné du temps avec un carnet. Dessiner les bits et noter les registres m'aidait à voir des erreurs que je n'aurais pas repérées en relisant le code seul.

Composer avec l'écran et le processeur

Le premier vrai casse-tête, c'est la mémoire vidéo. Sur le Spectrum, les lignes de pixels ne sont pas rangées en mémoire de façon linéaire comme on pourrait s'y attendre. La zone bitmap est organisée en trois blocs, et les lignes sont entrelacées d'une manière particulière. Les attributs de couleur, eux, sont stockés séparément : on a des cellules de 8 × 8 pixels, et chaque cellule partage ses couleurs de fond et d'encre.

C'est une des raisons du « color clash » qu'on voit dans beaucoup de jeux Spectrum. Certains calculs sont moins intuitifs, mais ce n'est pas arbitraire. Quand on connaît la structure, certains déplacements deviennent rapides : selon l'octet que vous incrémentez, vous progressez dans une ligne de pixels, ou vous « passez » à une autre zone. Ça devient clair quand on arrête d'essayer de forcer une logique « droite » et qu'on suit la mémoire telle qu'elle est.

Le problème apparaît quand on veut convertir une coordonnée X,Y en adresse vidéo. Oui, vous pouvez écrire une routine générique qui fait le calcul... mais la refaire pour chaque pixel et à chaque image coûte très cher. Pour un affichage statique, ça passe. Pour un effet animé, ça mange rapidement le budget.

Faire défiler un texte pixel par pixel

Un texte qui défile, ça fait partie du langage des cracktros. Ma première version affichait chaque lettre en entier, puis la déplaçait. Ça marchait, mais les caractères « sautaient » visuellement. Je voulais un mouvement fluide, pixel par pixel.

La solution utilise l'instruction RL, qui fait tourner les bits d'un octet en utilisant la retenue. En chaînant les octets voisins via cette retenue, on déplace réellement les bits image après image. En répétant l'opération sur les octets de la ligne, on décale le contenu horizontalement.

; Décaler une ligne de 32 octets

ld hl,fin_ligne

ld b,32

or a ; effacer la retenue

boucle_decalage:

rl (hl)

dec hl

djnz boucle_decalage

Ensuite, pour que chaque nouvelle lettre arrive progressivement, je ne pouvais pas me contenter d'appeler l'impression de la ROM. Je lisais la représentation de la lettre dans la police, je la plaçais dans un tampon de huit octets (un octet par ligne de pixels), puis le programme injectait les colonnes nécessaires dans le défilement.

Le point important n'est pas « il faut toujours faire du bas niveau ». C'est « sur cette machine, c'est ça ou rien ». Sur une plateforme moderne, déplacer un texte est trivial. Ici, vous manipulez les données qui construisent l'image. La contrepartie, c'est de mieux comprendre ce que coûte réellement chaque image.

Des étoiles sans calcul inutile

Mon premier champ d'étoiles déplaçait des lignes entières. Le principe était simple : faire avancer chaque ligne à une vitesse différente pour créer un effet de parallaxe. Deux soucis sont arrivés vite. D'abord, les lignes entraînaient avec elles d'autres éléments affichés. Ensuite, le nombre d'opérations finissait par dépasser le temps disponible par image. Et clairement, la musique ralentissait avec les effets : signe que la boucle principale n'était pas assez rapide.

J'ai donc changé de direction : au lieu de bouger des lignes, j'ai déplacé des étoiles une par une. Et j'ai évité de recalculer des positions aléatoires en continu. Un petit script Ruby génère au démarrage des coordonnées, choisies pour éviter certains conflits, puis le programme transforme ça en données prêtes à l'emploi.

Chaque étoile stocke directement l'adresse de l'octet vidéo où elle se trouve, son masque de bits, et un pointeur vers une table qui indique si la cellule est occupée par du texte. Résultat : plus besoin de recalculer une adresse X,Y à chaque image. Pour décider si une étoile doit être dessinée, je consulte la table d'occupation ; si la cellule contient du texte, l'étoile est déplacée sans être affichée.

C'est une optimisation assez concrète : déplacer moins de données, éviter des calculs répétés et ne pas redessiner toute l'image. Les positions ne sont pas « vraiment » aléatoires à l'exécution, mais le rendu paraît naturel et reste sous contrôle. Sur une machine comme celle-ci, obtenir un effet convaincant compte plus que respecter une idée abstraite de l'aléatoire.

Musique, effets et mémoire : les compromis

Une intro sans musique, c'est vite fade. Le Spectrum 128K utilise une puce AY à trois canaux, donc ce n'est pas pareil que sur Amiga. J'ai dû adapter : impossible de garder la musique à l'identique si elle était pensée pour quatre canaux.

J'ai choisi un morceau court issu d'une compilation Amiga que j'écoutais depuis longtemps. J'ai commencé par recopier les notes dans Vortex Tracker II, puis j'ai simplifié l'arrangement pour tenir sur trois canaux. Dans un tracker, les instruments ne sont pas des enregistrements audio : ce sont des descriptions de sons qui évoluent image par image (tonalité, bruit, volume).

Réduire quatre voix à trois

Dans Vortex Tracker II, il y a des ornements qui modifient la hauteur d'une note. En alternant entre une note et une autre, on peut donner l'impression qu'il y a un accord, même si le canal ne joue qu'une voix à la fois. En utilisant cette technique, j'ai pu suggérer des accords tout en libérant un canal pour d'autres éléments du morceau.

Et bien sûr, ce n'est pas une conversion neutre. Les choix de sons et de canaux changent le caractère de la musique. Au final, c'est un compromis musical autant que technique : garder ce qui rend le thème reconnaissable, plutôt que reproduire chaque détail de la version originale.

Le lecteur musical se lance une fois par image, avec les autres effets. C'est justement pour ça que les essais du champ d'étoiles étaient importants : si une routine graphique mange trop, ça finit par se voir aussi dans le son. Dans un système partagé, les performances ne sont pas « un sujet à part ».

Ajouter des vu-mètres et des scintillements

Les vu-mètres donnent un mouvement visuel calé sur le son. Plutôt que de construire un analyseur en temps réel, j'ai lu directement les registres d'amplitude de la puce AY, puis j'ai utilisé ces valeurs pour déterminer la hauteur des barres. La solution est simple, et surtout elle colle au matériel : la puce fournit déjà l'information utile.

Le logo a aussi quelques étoiles qui grandissent puis rétrécissent. Pour varier leur ordre d'apparition, je parcours une portion de la ROM et j'en récupère des octets qui servent de valeurs pseudo-aléatoires. Ce n'est pas un générateur « pour tout faire ». C'est une astuce adaptée à un besoin précis, avec des contraintes qu'on connaît.

Découper le programme pour tenir en mémoire

Quand j'ai ajouté les effets, l'organisation initiale a commencé à s'écrouler. J'avais mis l'essentiel (images, musique et lecteur) dans un seul bloc, puis copié chaque élément à sa destination. À force, certaines données finissaient stockées deux fois. Et le lecteur, en particulier, devait rester à une adresse fixe, ce qui réduisait encore l'espace disponible.

La solution : séparer le programme en plusieurs blocs, puis charger chaque partie aux adresses nécessaires via un chargeur BASIC en plusieurs étapes. Ce n'est pas aussi propre qu'un exécutable unique, et le fond d'écran peut apparaître avant la fin du chargement. Mais ça rend le tout plus simple à faire tourner, tout en respectant la contrainte mémoire.

C'est une décision de production classique, même si on parle d'une échelle « rétro ». Une architecture parfaitement propre sur le papier ne vaut pas grand-chose si elle ne rentre pas dans la RAM. Le bon réflexe, c'est d'identifier ce qui coûte vraiment, puis de déplacer la frontière entre simplicité, taille et confort d'utilisation.

Conclusion

Créer cette intro m'a remis face à une vieille ambition... et m'a surtout fait redécouvrir une manière de programmer que les langages modernes rendent facile à oublier. Ici, il faut regarder ce qui se déplace réellement : des octets, des bits, et le temps consommé à chaque image. On gagne en contrôle, mais on paye ce contrôle en temps de développement et en complexité.

Je ne vais pas réécrire mes projets pro en assembleur. Ce n'est pas le sujet. La contrainte était claire, l'objectif était limité, et l'apprentissage valait l'effort. Et au final, ça m'a rappelé une règle utile au-delà du rétro : optimiser ce qui gêne vraiment, accepter les compromis nécessaires, et livrer une version qui fonctionne avant de chercher à la rendre « encore plus parfaite ».

Si vous avez un Spectrum ou un émulateur, essayez la production et son fichier TAP. Et si vous codez sur une machine ancienne, partagez vos astuces : les meilleures optimisations viennent souvent d'une contrainte qu'on a pris le temps de comprendre.

Questions frequentes

Non, cette production vise un ZX Spectrum 128K, notamment parce qu’elle utilise la puce sonore AY à trois canaux et une organisation mémoire adaptée. Un modèle 48K ne possède pas le même matériel audio. Il faudrait réduire ou remplacer la musique et vérifier la mémoire disponible avant d’envisager un portage.
Un assembleur comme Pasmo, un émulateur tel que FUSE et une chaîne de compilation automatisée suffisent pour démarrer. Pour tester le résultat final, une vraie machine ou un matériel compatible reste utile. Un éditeur interactif et une documentation de la ROM aident à comprendre les instructions, mais un débogage méthodique reste indispensable.
La mémoire des pixels n’est pas organisée en lignes contiguës dans l’ordre habituel. Elle est entrelacée en blocs, tandis que les attributs de couleur sont stockés séparément par cellules de 8 × 8 pixels. Cette disposition complique les conversions de coordonnées, mais certaines opérations deviennent rapides quand on exploite directement son organisation.
Un outil comme Vortex Tracker II permet de composer pour les trois canaux de la puce AY. Les instruments sont définis par des paramètres de tonalité, de bruit et de volume plutôt que par des enregistrements. Les ornements peuvent simuler certains accords, mais chaque arrangement demande des compromis adaptés au nombre limité de voix.

Articles similaires