Magic System !

Magic System !

Il y a quelques jours, je suis tombé sur une "démo" d'invitation à un meeting de passionnés, qu'on appelle une "invitro" dans le jargon "démoscènique".

Benediction Coding Party 4 Invitro by Praline & Impact
invitation for Amstrad CPC / Amstrad Plus, released in may 2024

L'objectif de ce type de programme, tout comme les fanzines disques, est en général de se diffuser le plus largement possible auprès des passionnés d'une communauté.

Aussi, ce fut une surprise de découvrir que la dite démo ne fonctionnait sur aucun émulateur à part celui que l'auteur développe. 🙄

Ce dernier a en effet utilisé une découverte qu'il a faite sur le circuit FDC 765, le circuit qui permet de gérer les lecteurs de disquette, pour rendre l'invitation inaccessible aux utilisateurs des autres émulateurs que le sien (et même à certains hardwares réels mais je vais le laisser découvrir ça par lui même 🤣).

Ce billet va évoquer rapidement le FDC 765 et vous présenter un cas concret d'adaptation d'une rupture ligne à ligne sur le CRTC 2.


Magic Fanzine ?

Ce n'est pas la première fois que ce programmeur se tire ainsi une balle dans le pied à cause d'un manque de rigueur dans son travail de recherche.

Il avait par exemple diffusé en novembre 2023 un fanzine disque qui détecte les émulateurs via leur émulation du FDC 765.

Cependant, la méthode de détection utilisée démontre surtout que l'auteur confond le circuit FDC et les LECTEURS que ce circuit pilote.

En effet, pour déterminer si il est face à un émulateur ou non, l'auteur a supposé que seul un lecteur émulé donne des délais constants, sans tenir compte des nouveaux lecteurs qui utilisent de la mémoire.

Concrètement, si son test (présent en #80C9 dans le code) identifie que le délai est similaire en poids faible et est inférieur en valeur à #800, il considère le circuit FDC comme étant émulé.

Le programmeur a triché en "fakant" un test CRTC via son test FDC sur un émulateur ciblé avec une valeur précise (en #8034 dans son code pour les plus curieux). Un article évoque le sujet ici.

Sur de vraies machines équipées de lecteurs numériques, un "FDC réel" est donc parfois détecté comme un circuit émulé. Se pourrait il que le FDC soit affecté d'un vieillissement modifiant son comportement ? 🤣

Fanzine disque détectant un Fdc D765AC de NEC comme émulé.
Fanzine disque sur Hardware réel détectant le lecteur comme émulé...

Tant que le fanzine se contentait d'afficher le résultat de son diagnostic, rien de bien grave si l'utilisateur avait encore accès au contenu, mais il semble également que certains "lecteurs modernes" ne soient pas assez "accurate" pour le loader... Et sans méthode alternative au chargement musical, c'est la fin du film.

Il est également étonnant de voir un programme, et à plus forte raison le fanzine d'un spécialiste du FDC, ne pas savoir gérer le drive B en 2023.

Tout le monde ne dispose pas d'un inverseur de drive hardware. C'est mon cas.

Et le CRTC ?

Lors de sa sortie, j'avais été surpris de constater l'incompatibilité avec un drive secondaire, mais également de constater qu'il plantait sur le CRTC 2.

Je précise que l'auteur a depuis corrigé ce problème dans une nouvelle version (en positionnant R3 avec 8), mais elle ne tourne toujours pas sur drive B.

Le problème rencontré était des plus classiques sur le CRTC 2, puisque c'est historiquement une des premières différences entre CRTCs qui a été détectée sur les CPC, et qui a par exemple conduit Rémi Herbulot a tenter de les différencier dans son jeu "Crafton & Xunk", moyennant un dégât collatéral sur le CRTC 1.

Lorsqu'un écran fullscreen est positionné à gauche, on modifie en général le registre CRTC R2 pour y mettre la valeur 50. Mais il est nécessaire de respecter la règle R2+R3<=R0, sinon cela déclenche une VSYNC Fantôme.

Autrement dit, si la condition n'est pas respectée, l'écran est désynchronisé verticalement et défile de manière ininterrompue :

Première version du fanzine Fatmag2 sur CRTC 2

Or sur un CPC au reset avec une rom standard, la valeur de R3 est de 14. (R0 vaut en général toujours 63). On a donc 50+14=64. 64 étant supérieur à 63, le signal de synchronisation vertical n'est plus envoyé au moniteur par le GATE ARRAY car il ne le reçoit plus du CRTC (qui pourtant "croit" la faire).

Etant donné que le programmeur ne s'était pas ennuyé à initialiser préalablement les registres du CRTC, il suffisait pour régler le problème de taper sous Basic les instructions suivantes avant le chargement de l'ancienne version:

OUT &BC00,3:OUT &BD00,6

Si vous souhaitez en savoir davantage sur cette règle, je vous conseille la lecture du chapitre 16.4.3 du Compendium (https://shaker.logonsystem.eu/)


Magic Loader 🍈

Pour revenir à l'invitro, son programmeur a qualifié son loader de "Magic loader".

Attiré par l'odeur de la magie, il a été craqué par "Magic Soft", qui a repris du service pour l'occasion. 🤣

Ce loader utilise une spécificité de l'architecture du FDC au sein du CPC qui ne sait gérer que 2 drives au lieu de 4 car l'une des broches de sélection de drive du FDC 765 (la broche US1) n'est pas "connectée" (Une autre économie de M. Sucre) et cela permet de s'affranchir, sous conditions, d'une instruction de changement de piste.

Contrairement à ce que les recherches sur d'autres circuits peuvent apporter de plus à l'expérience utilisateur, ce genre de "truc" n'améliore pas le chargement de données. Cela ne va ni plus vite, ni n'apporte plus de données, bien au contraire.

Cela reste néanmoins toujours une très bonne chose de chercher à améliorer la qualité d'émulation des différents circuits qui équipent le CPC, y compris le FDC 765. La méthode employée ici est cependant assez déplorable et à ranger dans l'étal des melons, notamment si on en croit le contenu présomptueux du readme livré avec la démo.

L'auteur aurait par exemple pu élaborer un programme spécifiquement dédié à ses recherches et les documenter, mais il n'en a rien fait.

Tous invités ! 🙄

Avec la version "Magic Soft", tous les CPC réels ou les émulateurs disposant de 128K peuvent désormais voir l'invitation. Tous les auteurs d'émulateurs (ou ceux qui n'ont que 64k) peuvent continuer à jouer avec le "Magic Loader" si "complexe" de la version d'origine. 🤣

A mon humble avis, si vous deviez concevoir prochainement un loader, je ne peux que vous conseiller de changer de piste "normalement"... 😂

Lorsque cette démo d'invitation démarre, elle indique qu'elle ne fonctionne pas sur CRTC 2, ici identifié comme MC6845 :

Après visionnage de la démo, rien de bien difficile techniquement puisque c'est une technique dite de rupture ligne à ligne qui est utilisée pour gérer l'animation. Nous allons étudier cela...

Invitro sur CRTC 2

Cette technique a longtemps été réputée impossible, jusqu'à ce que le contraire soit démontré fin 2021 dans Amazing Demo V3 et le Compendium CRTC.

Afin de permettre l'adaptation au CRTC 2 dans les meilleures conditions, il était nécessaire de retirer le "Magic loader".

L'auteur a laissé un code sensé permettre de "regénérer" la disquette qui utilise des fichiers dont on ne connait pas le contenu. Il a été impossible d'utiliser le "bnd4_floppyMaker" pour recréer le dsk-magic-loader sans plantage avec la dernière version d'un émulateur pourtant certifié "accurate" :

Accurate!

Bref, place à Magic Soft, une petite intro écrite par Merlin en 1986...

Tant qu'à faire dans le magique, je conseille à l'auteur d'utiliser "Magic Dos" si il lui arrive de manquer de place à cause des secteurs mono-piste de 4 kb du "Magic Loader".


Magic CRTC ⭐

Rupture ligne à ligne.

Je m'en excuse par avance, mais les propos qui vont suivre seront un peu plus techniques, et il vous faut maitriser un minimum le CRTC et le Z80A si vous voulez comprendre mes propos.

La technique dite de rupture ligne à ligne permet de modifier l'adresse de chaque ligne affichée. En allant piocher l'adresse sur 2 octets de la ligne dans une table vectorisée à partir d'une autre table de lignes sur 1 octet, la mise à jour de cette table (ou de son pointeur d'origine) permet de "déplacer" chaque ligne individuellement et facilement sur l'écran.

Afin de réaliser cette opération, il faut (en général) que le CRTC débute un nouveau "frame". Un frame est un ensemble de lignes déterminé par le moment ou le compteur de caractère C4 repasse à 0. Chaque caractère peut contenir plusieurs lignes, dont le numéro est stocké dans C9. La frontière de C4 est R4 et la frontière de C9 est R9.

Autrement dit si R4 vaut 1, C4 passera à 0, puis 1, puis 0, puis 1, ...

Lorsqu'on est en "rupture ligne à ligne", 1 ligne correspond à 1 frame.

"Si on souhaite obtenir des caractères de 1 ligne, est-ce qu'il suffit juste de mettre R4 et R9 à 0 lorsque C9 et C4 sont à 0 ?" se demande un grand chauve à lunettes qui sait désormais tout sur tout. Le petit blond qui ne comprenait jamais rien à rien a bien changé, il a perdu ses cheveux en vieillissant.

Il a raison, mais uniquement sur les CRTC 1, 3 (et 4).

Sur les CRTC 0 et 2, la logique de comptage n'est pas aussi simple.

Sur la dernière ligne du "frame", la remise à 0 de C4 est décidée au début de la ligne, et non à la fin de cette dernière. Donc modifier R4 et R9 au cours de la dernière ligne du frame n'empêche pas C4 et C9 de revenir à 0.

Mais ça peut être utile si on souhaite que la prochaine ligne soit également considérée comme une dernière ligne.

Ainsi il faut positionner R4 et R9 avec la valeur 0 sur la ligne qui précède celle ou C4 et C9 devaient passer à 0.

Exemple concret...

Si on prend l'exemple de cette démo, on remarque ci-dessous dans le premier cadre en rouge une boucle d'attente du début de la Vsync, qu'il ne faut pas confondre avec le début du frame.

La Vsync se déclenche lorsque C4 atteint R7. Sur un CPC standard, la valeur de R7 est de 30, ce qui implique que la Vsync est envoyée au moniteur lorsque le CRTC a géré 240 lignes (30 x 8 lignes). On verra plus tard que cette méthode d'attente de la Vsync est un peu archaïque et manque singulièrement de précision.

La mise à jour de R7 avec une valeur haute après l'attente de la Vsync ne sert à rien, puisque l'idée est de passer en rupture ligne à ligne et de laisser C4 reboucler à 0. Il ne risque plus d'atteindre 30 (ou la future valeur qui y sera mise).

Le test CRTC qui est réalisé au début de la démo permet de mettre #B ou #C dans le premier "ld b,#0c" en jaune, afin d'attendre une ligne de moins la fin du frame en cours. Cela permet donc de positionner le code au niveau de cette dernière ligne afin de gérer la particularité de gestion des CRTC 0 et 2.

On remarque accessoirement une initialisation un peu bordélique de SP et de B', qui vont représenter, respectivement, un pointeur sur des paramètres de couleurs et l'adresse I/O du Gate Array et du PAL (#7F) (ici pour mettre à jour les couleurs).

Comme on le voit ci-dessous, R9 et R4 sont positionnés à 0 (premier cadre rouge), afin que les prochains C4 et C9 soient à 0.

En vert, 2 pointeurs sur des tables qui seront lues dans la boucle de 64 µsec qui part du label lca56 et qui reboucle avec la flèche verte. L'accumulateur (ld a,#ef) contient le nombre de lignes en rupture ligne à ligne.

Finalement, c'est pas plus compliqué que ça une démo de 1991...🤣

Le second cadre en rouge montre, dans cet ordre, la sélection du registre R12 du CRTC, la lecture de la ligne à afficher (LD A,(DE)) et le passage à la ligne suivante (INC E) qui boucle sur 256 valeurs. Grâce à cet index de ligne, cela permet de lire un offset 16 bits pointé par HL afin de mettre à jour d'abord R12, puis R13.

Le second cadre en rouge montre la lecture de deux valeurs 16 bits qui contiennent des couples "pen"+"couleur" modifiés en fin de ligne.

Et le CRTC 2 ?

Mais pourquoi ça ne marche pas sur CRTC 2, se demande le grand chauve à lunettes, qui a bien mal révisé son Compendium illustré.

Parce que le CRTC 2 a besoin que R9 soit différent de 0 durant la Hsync pour éviter que C4 ne déborde. Si vous souhaitez creuser le sujet en détail, je vous invite à lire le Compendium sur le sujet, mais je vais vulgariser le sujet.

Cela implique concrètement de pouvoir modifier R9 deux fois sur la ligne, et que R9 soit remis à 0 lorsque la Hsync est terminée, avant la fin de la ligne.

Si vous vous souvenez de ce que j'écrivais sur R2 et R3 plus haut, R3 définit la taille de la Hsync. On la plaçant au minimum requis (à savoir 6), elle se termine plus vite et il reste du temps pouvoir remettre R9 à 0 avant la fin de la ligne.

Lorsque l'écran est en fullscreen horizontal, on a vu que R2 vaut en général 50 pour un bon centrage. Si R3 vaut 6, il faut que R9 soit différent de 0 jusqu'à ce que C0 dépasse 55, lorsque le signal Hsync du CRTC a cessé :

Le schéma ci-dessus représente une ligne vidéo avec ses 64 µSecondes (0 à 63). Lorsque la position C0=50 est atteinte (R2), une Hsync débute et va durer R3 µsecondes (soient 6 dans notre cas). Le programmeur doit se débrouiller pour que R9 soit supérieur à 0 sur la position 55 (ou plus), mais qu'il soit revenu à 0 sur la position 63 au plus tard.

Pour adapter et patcher le code présenté plus haut, il faut donc d'une part récupérer de la CPU pour faire 3 OUT de plus, et d'autre part synchroniser de manière moins cochonne ce code avec le CRTC.

Optimisation

Je ne vais pas disserter sur la méthode employée pour obtenir l'effet, étant donné que le code d'origine semble avoir été vite expédié. 🐷

Voici une solution toute aussi rapide pour récupérer la CPU manquante, que je vais vous détailler :

J'ai "unrollé" le code afin de récupérer le temps de la boucle. Ce qui signifie que le code ci-dessus est recopié autant de fois qu'il y a de lignes dans la ram.

Il existe quelques méthodes pour faire une vraie boucle et dépenser moins de ram, mais ce n'était pas l'objet de la manœuvre, et cela aurait demandé de toucher à l'organisation des données et/ou du code.

En résumé:

  • La mise à jour de R12/R13 (offset) est optimisée en utilisant la particularité du OUT (#FF),A (voir chapitre 23.1 du Compendium) qui prend 3 nop et n'impose pas de modifier B.
  • La mise à jour des couleurs ne gaspille plus que HL', laissant DE' libre de contenir le pointeur de ligne, pour permettre à DE de servir à autre chose (E=9 pour la sélection de R9, et D=#BE pour recharger l'adresse I/O du CRTC).
  • R9 est ainsi affecté avec une valeur non nulle via le registre B (qui contient #BD, ce qui correspond à #1D car R9 à une résolution de 5 bits), et est remis à 0 en fin de ligne (instruction OUT(C),0 = #ED,#71)

Synchronisation

Il était également nécessaire d'éviter les fluctuations engendrées par l'attente cochonne de la Vsync.

Pour illustrer mon propos, je vais reprendre le code d'attente de Vsync de cette démo, mais étalé dans le temps, µseconde par µseconde pour plusieurs images :

Le "in a,(c)" dure 4 µsec et c'est sur la dernière µsec de l'instruction, symbolisée par un x sur fond jaune, qu'il reçoit l'information sur l'état du signal Vsync dans le registre A du Z80A.

Le signal Vsync s'active très exactement toutes les 19968 µsec. Si le code qui a rebouclé sur cette routine d'attente n'est pas un multiple de 8 µsec, le signal Vsync peut s'activer sur n'importe quelle µsec durant la boucle.

Les 3 exemples avec les "x" en orange symbolisent l'activation de Vsync sur différents écrans et montrent que la sortie de boucle peut subir un décalage de plusieurs µsecondes entre 2 images. Ceci peut avoir une incidence sur le moment ou les circuits (CRTC ou GATE ARRAY) vont recevoir des mises à jour.

Afin d'éviter ces fluctuations inhérentes à la boucle, il existe des solutions, comme la programmation en temps fixe, ou utiliser les interruptions, qui sont aussi dépendantes du CRTC. C'est cette seconde méthode que j'ai utilisée.

Petit rappel sur les interruptions...

Sur CPC, une interruption a lieu toutes les 52 lignes (et la première débute 2 lignes après le début de la Vsync sous conditions). Si ce compteur a été dépassé sans que les interruptions soient autorisées, la première instruction EI va déclencher une interruption immédiatement (après l'instruction qui suit le EI).

Il faut donc éviter qu'une interruption soit en attente lorsque le code attend le signal Vsync. Pour cela il faut acquitter l'interruption à chaque fois que le compteur a atteint 52.

En #38, le code original avait placé un EI + RET. Les interruptions sont réautorisées en #38 jusqu'à l'entrée dans la boucle principale. Un zéro placé en #38 élimine le EI pour le remplacer par un NOP. La boucle d'attente de la Vsync va cependant autoriser les interruptions dans la boucle d'attente.

Lorsque la Vsync a lieu, l'instruction HALT va attendre la prochaine interruption, qui va se produire 2 lignes après. Cette instruction HALT génère des NOP de 1µSec en masse avant l'interruption et on est donc ainsi calé à la µseconde près.

Etant donné que deux lignes ont été perdues après la Vsync, il suffit de diminuer la valeur du compteur d'attente de la dernière ligne du frame en modifiant le LD B,#C qui devient LD B,#A (voir ci-dessous).

Il n'y a plus de EI dans la routine d'interruption située en #38, aussi plus aucune interruption ne se produira. Un DI aurait aussi pu faire l'affaire.

Le bon moment pour réarmer les interruptions...

Je vous ai raconté un peu plus haut qu'une interruption se produisait 2 lignes après le début de la Vsync sous "condition".

Une chose à retenir c'est que si les interruptions sont interdites et que le compteur de lignes a dépassé 51 et rebouclé à 0, une interruption est en attente et le premier EI va déclencher une interruption immédiate ne permettant pas de bénéficier du rebouclage du compteur comme déclencheur.

Le second "effet" indésirable, c'est que lorsque cette interruption immédiate se produit, le bit 5 du compteur est mis à 0. Autrement dit, si ce compteur n'a pas encore dépassé 31 lorsque l'interruption a lieu, la mise à 0 du bit 5 n'a pas d'effet. Par contre, si le compteur valait 32 ou plus, il se retrouve amputé de 32 lignes.

Si ce bit vaut encore 0 deux lignes après le début de la Vsync, alors cette dernière n'a pas lieu. Je vous invite à consulter le chapitre 26 du Compendium si vous souhaitez aller plus loin sur les interruptions du CPC.

Après avoir désactivé les interruptions pendant la rupture ligne à ligne (qui fait plus de 52 lignes) et avant de revenir dans la boucle d'attente de la Vsync, il est nécessaire de purger l'interruption en attente avec EI.

Mais pour éviter de mettre le bazar dans le compteur, il faut le faire dans une période ou le compteur de lignes est inférieur à 32.

Autrement dit, toutes les 52 lignes, il y a 32 lignes ou il est possible de poser un EI suivi de 20 lignes ou le EI va modifier le compteur. Sur le schéma ci-dessous, on peut considérer que les zones en vert sont les 32 lignes ou un EI est possible sans affecter le compteur de ligne.

En sortie des boucles de rupture ligne à ligne, un EI est donc placé dans une zone en vert, afin de ne pas saccager le compteur interne du GATE ARRAY.

Conclusion

Adapter un code au CRTC 2 n'est pas plus compliqué que ça. Et pas bien long non plus à faire, car j'ai du taper moins de 30/40 lignes de code pour y parvenir.

C'est encore plus simple si on ne doit pas patcher du code pré-existant avec des bugs, qui blinde littéralement la mémoire de textes directement intégrés comme du bitmap. Si "grouik" est le cri de guerre de l'auteur - qu'il répète à l'envi - on pourra s'accorder qu'il s'agit même de son crédo. 🐷

Cet article a été nettement plus long à écrire que d'adapter cette démo...

Il est possible que le CRTC 2 ait été ignoré dans cette démo car l'émulateur "accurate fdc" ne sait pas encore l'afficher correctement. Et si ce circuit ne mérite pas l'attention de l'auteur, pourquoi prétendre l'émuler et ne pas directement le retirer de la liste des circuits gérés par l'émulateur...

J'espère sincèrement que cet article vous aura appris quelques petites choses, afin de démystifier un peu ce sujet, et que cela pourra vous aider si vous programmez encore sur cette vieillerie qu'est le CPC ou que vous bossez sur un émulateur.

Longshot / Logon System