ACCEPT, enfin !
Cela aura pris un peu de temps, entre autres raisons car d'autres activités se sont invitées entre-temps, mais ACCEPT est enfin implémenté ! ACCEPT est un mot qui lit une ligne de texte entrée par l'utilisateur et la stocke dans une adresse mémoire fournie lors de l'appel. Généralement, cette adresse est le TIB (Terminal Input Buffer), qui sera ensuite fournie à l'interpréteur Forth.
Comme indiqué lors du précédent article, se posait la question de savoir comment récupérer les données. J'ai tenté deux versions et je suis finalement partie sur celle que je pressentais : utiliser la mémoire PPU pour stocker les données en cours d'édition, puis les récupérer lorsque la touche RETURN est appuyée.
Oui, mais... même si la mémoire représentant les caractères est linéaire, depuis le début de l'implémentation j'ai réservé des caractères sur les bords de l'écran pour ne rien y afficher, afin de respecter les marges nécessaires lors d'un affichage sur écran cathodique. Récupérer les données nécessite donc de prendre des morceaux de mémoire à différents endroits. Pas forcément très complexe, mais ça nécessite un peu plus de réflexion qu'un simple transfert en une fois.
Et tant qu'à faire, puisque je dois définir une géométrie d'écran logique, pourquoi ne pas implémenter un mot WINDOW qui permettrait de définir les marges haut, bas, gauche et droite ? C'est ce que j'ai commencé par faire, en adaptant la gestion du curseur qui était codée en dur avec des marges fixes. Deux routines me permettent d'avancer et de reculer le curseur en respectant cette géométrie de fenêtre logique, et c'est bon. Les marges sont toujours là, initialisées au démarrage, mais à présent elles sont configurables.
Retour sur ACCEPT. Au début, il s'agit uniquement de reprendre le code d'affichage de caractères que j'avais déjà. Le code me semble quand même voué à être un peu long, je lui dédie donc un fichier. Il me faut implémenter l'édition d'une ligne logique, avec gestion des touches curseur du clavier (au moins droite et gauche), ainsi que des touches INS (insertion), DEL (suppression) et RETURN (validation de la ligne). Ces touches ne sont pas encore branchées, je repasse donc sur la gestion du clavier.
Gestion du clavier complet
Tant qu'à faire, puisque je suis sur le clavier, j'en profite pour gérer les majuscules, minuscules et kanas. Pour cela, il me faut compléter la fonte. Celle que j'avais choisie n'a pas tous ces caractères. J'en cherche une et je tombe sur la fonte Misaki, qui, en plus des katakanas, contient des caractères graphiques. Parfait pour utiliser la touche GRPH du clavier.
La fonte format bitmap 8x8 est fournie avec beaucoup de caractères. J'écris donc un petit script Python pour en prendre une sélection et générer ma fonte dans le format Famicom attendu. Puis je mappe tous les scancodes, un par un, vers leurs équivalents pour quatre tables différentes : majuscules, minuscules, kanas et graphiques. Je sélectionne quelques codes entre 0 et 32 pour les touches de contrôle, en respectant les codes classiques et en improvisant un peu pour les autres.
Et me voilà avec un mapping complet. Je peux compléter ACCEPT pour gérer les touches SHIFT, KANA et GRAPH. Les touches de direction avant et arrière ne posent pas beaucoup de problèmes... à part le curseur.
Tant que le curseur était en fin de ligne, c'était simple. Mais maintenant qu'il se déplace sur des caractères déjà présents, je veux le faire clignoter afin de laisser le caractère en dessous apparent. Et pour savoir quel est ce caractère, il faut aller le chercher... dans la mémoire de l'écran !
Transferts PPU <-> RAM
Je laisse donc le clavier de côté. Il est temps de s'occuper des transferts RAM/PPU. J'ai déjà, via le framework utilisé, un transfert de buffer de la RAM vers la mémoire PPU. Celui-ci fonctionne par recopie de données dans un buffer de commandes. C'est bien pour la sécurité des données : comme elles sont copiées, la source peut changer en RAM, elles sont au chaud dans la commande. Par contre, cela prend du temps de recopie et de la place en RAM.
J'implémente donc une version qui permet de référencer une adresse mémoire et une longueur, qui servent directement lors du décodage des commandes pendant la synchronisation verticale. À la charge de l'appelant d'assurer que les données sont toujours là où elles doivent être. J'en profite pour afficher le message de bienvenue avec cette commande, afin de valider le fonctionnement.
De là, il est assez facile d'en faire la commande symétrique : une commande qui référence une adresse mémoire et une longueur en RAM, et qui y copiera les résultats de requêtes PPU lors de la prochaine synchronisation verticale. En spécifiant comme adresse celle d'une variable contenant le caractère sous le curseur et comme longueur 1, je peux récupérer ce caractère courant.
Pour être sûr des buffers, j'attends la synchronisation verticale après chaque déplacement de curseur. Pendant l'édition d'une ligne d'entrée logique, ce n'est pas très important.
Retour à l'édition de ligne logique
Bien. À présent, j'ai la possibilité de récupérer le caractère sous le curseur, mais aussi tous les caractères à l'écran. Avant d'implémenter RETURN, je fais un détour par INS et DEL, afin de pouvoir faire de l'édition et de ne pas partir du principe que l'utilisateur tape une ligne parfaite du premier coup. Mes multiples tests montrent que ce n'est pas le cas. J'ai besoin de ces touches !
Un détour par le Family BASIC me montre que INS insère un espace à l'emplacement du curseur, ok, mais que DEL est en fait un BACKSPACE : il supprime le caractère avant le curseur et recule le curseur d'une position. Par cohérence, j'implémente le même comportement.
Cependant, il n'est pas possible de déplacer des morceaux de mémoire PPU directement. Ou alors je n'ai pas trouvé (et les personnes à qui j'ai demandé ne savaient pas). Qu'à cela ne tienne, ACCEPT a à sa disposition un buffer de travail dans lequel il déposera les données finales. Je l'utilise comme buffer temporaire. Pour les deux opérations, je récupère les données de l'écran (en respectant la géométrie de la fenêtre logique), je fais la modification en RAM, puis je renvoie le tout à l'écran.
Je ne m'embête pas à trouver la sous-partie qui va bouger, je récupère et je renvoie tout. J'attends même une synchronisation verticale pour chaque ligne physique transférée pour être certain de tenir dans le temps de synchronisation. Sur émulateur, ça ne bronche pas. On verra sur vrai matériel. Au pire, je pourrais grouper quelques transferts pour trouver le bon équilibre. Mais là encore, on est en pleine édition de ligne logique utilisateur, on peut se permettre des attentes sans affecter l'expérience utilisateur.
ACCEPT !
L'avantage avec l'implémentation de INS et DEL, c'est qu'il y a déjà tout ce qu'il faut pour implémenter RETURN. J'ai la récupération des données en RAM. Reste à pousser la longueur actuelle de la ligne logique sur la pile. Cette longueur est maintenue au fur et à mesure des opérations d'édition.
Il reste des choses à faire pour une édition au clavier agréable et complète. Et une paire de petits bugs, dont la longueur de la ligne logique qui n'est pas mise à jour correctement dans certains cas. Je m'en occuperai plus tard. Il est grand temps d'avancer vers INTERPRET !
Amélioration du framework de test
Au passage, avec l'arrivée du clavier, j'ai ajouté comme prévu l'injection de caractères depuis le framework de tests. Cela m'a permis d'écrire un test pour INS et un autre pour DEL. Ces tests demandent la mise en place de contextes en plusieurs étapes, avec parfois des attentes de frames pour attendre les résultats. Les tests commençaient à être très verbeux et j'ai donc fait une passe pour factoriser tout ça dans des fonctions d'aide qui me permettent d'écrire le déroulement des opérations qui seront ensuite conduites de manière asynchrone.
Interprète... interprète !
Maintenant que j'ai une boucle d'entrée de texte, la prochaine étape est l'interprétation du résultat. Cela va nécessiter l'écriture de nombreux mots standards de Forth (au passage, j'ai implémenté DROP car actuellement je dois ignorer le retour de ACCEPT). Cependant, ces mots devraient être nettement plus simples que ACCEPT ou la gestion clavier.
La « simple » implémentation d'ACCEPT a eu de nombreuses ramifications. J'en ai même profité pour faire quelques petites optimisations/simplifications simples au passage.
J'aimerais pour la suite pouvoir écrire quelque chose comme HEX 7FF FF C!, qui écrit la valeur 0xFF à l'adresse 0x7FFF, ce que le framework de test peut détecter. Cela signifie le découpage par mots de la chaîne d'entrée, un traitement de base numérique, le parsing de nombres dans cette base et l'implémentation de C! (le plus facile de toute la liste).
Partons là-dessus !
