gint : un noyau pour développer des add-ins
Posté le 20/02/2015 17:30
Ce topic fait partie de la série de topics du fxSDK.
En plus des options de programmation intégrée comme le Basic Casio ou Python, la plupart des calculatrices Casio supportent des
add-ins, des programmes natifs très polyvalents avec d'excellentes performances. Les add-ins sont généralement programmés en C/C++ avec l'aide d'un ensemble d'outils appelé SDK.
Plusieurs SDK ont été utilisés par la communauté avec le temps. D'abord le
fx-9860G SDK de Casio avec fxlib pour Graph monochromes (plus maintenu depuis longtemps). Puis le
PrizmSDK avec libfxcg pour Prizm et Graph 90+E (encore un peu actif sur Cemetech). Et plus récemment celui que je maintiens, le
fxSDK, dont gint est le composant principal.
gint est un unikernel, ce qui veut dire qu'il embarque essentiellement un OS indépendant dans les add-ins au lieu d'utiliser les fonctions de l'OS de Casio. Ça lui permet beaucoup de finesse sur le contrôle du matériel, notamment la mémoire, le clavier, l'écran et les horloges ; mais aussi de meilleures performances sur le dessin, les drivers et la gestion des interruptions, plus des choses entièrement nouvelles comme le moteur de gris sur Graph monochromes.
Les sources de gint sont sur la forge de Planète Casio :
dépôt Gitea Lephenixnoir/gint
Aperçu des fonctionnalités
Les fonctionnalités phares de gint (avec le fxSDK) incluent :
- Toutes vos images et polices converties automatiquement depuis le PNG, sans code à copier (via fxconv)
- Un contrôle détaillé du clavier, avec un GetKey() personnalisable et un système d'événements à la SDL
- Une bibliothèque standard C plus fournie que celle de Casio (voir fxlibc), et la majorité de la bibliothèque C++
- Plein de raccourcis pratiques, comme pour afficher la valeur d'une variable : dprint(1,1,"x=%d",x)
- Des fonctions de dessin, d'images et de texte optimisées à la main et super rapides, surtout sur Graph 90+E
- Des timers très précis (60 ns / 30 µs selon les cas, au lieu des 25 ms de l'OS), indispensables pour les jeux
- Captures d'écran et capture vidéo des add-ins par USB, en temps réel (via fxlink)
Avec quelques mentions spéciales sur les Graph monochromes :
Un moteur de gris pour faire des jeux en 4 couleurs !
La compatibilité SH3, SH4 et Graph 35+E II, avec un seul fichier g1a
Une API Unix/POSIX et standard C pour accéder au système de fichiers (Graph 35+E II seulement)
Et quelques mentions spéciales sur les Graph 90+E :
Une nouvelle police de texte, plus lisible et économe en espace
Le dessin en plein écran, sans les bordures blanches et la barre de statut !
Un driver écran capable de triple-buffering
Une API Unix/POSIX et standard C pour accéder au système de fichiers
Galerie d'add-ins et de photos
Voici quelques photos et add-ins réalisés avec gint au cours des années !
Arena (2016) — Plague (2021)
Rogue Life (2021)
Momento (2021)
Communication avec le PC (cliquez pour agrandir)
Utiliser gint pour développer des add-ins
Les instructions pour installer et utiliser gint sont données dans les divers tutoriels recensés dans le
topic du fxSDK. Il y a différentes méthodes de la plus automatique (GiteaPC) à la plus manuelle (compilation/installation de chaque dépôt). Le fxSDK est compatible avec Linux, Mac OS, et marche aussi sous Windows avec l'aide de WSL, donc normalement tout le monde est couvert
Notez en particulier qu'il y a des
tutoriels de développement qui couvrent les bases ; tout le reste est expliqué dans les en-têtes (fichiers
.h) de la bibliothèque que vous pouvez
consulter en ligne, ou dans les ajouts aux changelogs ci-dessous.
Changelog et informations techniques
Pour tester les fonctionnalités et la compatibilité de gint, j'utilise un add-in de test appelé gintctl (
dépôt Gitea Lephenixnoir/gintctl). Il contient aussi une poignée d'utilitaires d'ordre général.
Ci-dessous se trouve la liste des posts indiquant les nouvelles versions de gint, et des liens vers des instructions/tutoriels supplémentaires qui accompagnent ces versions.
Anecdotes et bugs pétés
Ô amateurs de bas niveau, j'espère que vous ne tomberez pas dans les mêmes pièges que moi.
TODO list pour les prochaines versions (2023-04-03)
gint 2.11
- Changements de contextes CPU. À reprendre du prototype de threading de Yatis pour permettre l'implémentation d'un véritable ordonnanceur. Demandé par si pour faire du threading Java.
- Applications USB. Ajouter le support de descripteurs de fichiers USB. Potentiellement pousser jusqu'à avoir GDB pour debugger.
- Support de scanf() dans la fxlibc. Codé par SlyVTT, plus qu'à nettoyer et fusionner.
Non classé
- Regarder du côté serial (plus facile que l'USB) pour la communication inter-calculatrices (multijoueur) et ultimement l'audio (libsnd de TSWilliamson).
- Un système pour recompiler des add-ins mono sur la Graph 90+E avec une adaptation automatique.
- Support des fichiers en RAM pour pouvoir utiliser l'API haut-niveau sur tous les modèles et éviter la lenteur de BFile à l'écriture quand on a assez de RAM.
Citer : Posté le 10/03/2020 16:41 | #
Hmm c'est plus court c'est vrai, mais d'un côté on a dimage() et type:bopti-image. Je sais pas si c'est plus lisible aussi... mais bon, j'ai le temps de voir.
Citer : Posté le 10/03/2020 17:42 | #
Qui de type:bopti-img ?
Citer : Posté le 10/03/2020 17:51 | #
Je savais que quelqu'un allait dire ça. x)
C'est juste moins lisible pour un truc qui n'est même pas dans le code à mon avis. Le namespace img_ est court parce qu'on le tape un peu partout. Là y'a clairement pas le même besoin.
Citer : Posté le 10/03/2020 19:09 | #
Pourquoi avoir séparé gint et libimg du coup ? La déçision me surprend vu la modularité de gint à laquelle tu tiens absolument, distinguer les deux composants en deux projets me paraît superflu du point de vue technique du coup… tu comptes distinguer la gouvernance des deux projets, ou permettre à libimg de tourner sur autre chose que gint ?
Mon blog ⋅ Mes autres projets
Citer : Posté le 10/03/2020 19:15 | #
gint reste un noyau, je préfère m'en tenir au bas niveau sur ce point. Si tu veux, en poussant le modèle unikernel à l'extrême, gint est l'OS et les libs autour sont les logiciels. Tu mélanges pas les deux normalement (eg. ton serveur X n'est pas dans le noyau).
Ici, en plus, la lib ne dépend vraiment pas de gint à part pour le rendu sur VRAM qui fait 40 lignes en tout. Donc le mettre dans le dépôt de gint va juste empêcher un portage facile vers libfxcg si l'occasion se présente.
Bon, du reste je sais que clôner et compiler tous ces machins c'est casse-pieds. Je ne sais pas vraiment comment le faire proprement. Si je fais un script dans le fxSDK pour automatiser tout ça je vais réinventer un gestionnaire de paquets, juste tout nul. Mais en même temps j'ai pas les moyens de maintenir des paquets pour les nombreuses distros qui sont utilisées sur Planète Casio.
Citer : Posté le 11/03/2020 13:04 | #
Bonzour,
J'essaye dans un programme d'attendre que l'utilisateur clique sur une touche avant de continuer.
Le problème c'est qu'avant les KEY_EXE et autres "objets touches" étaient assimilés à des entiers, et donc on pouvait faire la portion de code suivante :
if (getkey() == KEY_EXE) break;
}
Cependant avec l'implémentation d'un type pour les touches, j'avoue ne plus bien comprendre comment récupérer l'appui d'une touche. J'ai fais ce code :
key_event_t my_key;
my_key.key = KEY_EXE;
while (1==1) {
if (getkey_opt(GETKEY_NONE, 1) == my_key) break;
}
1) Ce code génère une erreur. getkey_opt ne semble pas être du même type que KEY_EXE malgrè toutes les précautions que j'au pu prendre (fabriquer une variable de type key_event, lui associer le code de KEY_EXE, ...)
2) Cela me semble bien compliqué ... N'y a-t il pas moyen de comparer directement le getkey à un keycode ?
Teusner
Citer : Posté le 11/03/2020 13:14 | #
Oh, tu n'es pas loin, mais tu ne devrais pas comparer deux événements entre eux. Tu dois juste accéder à la touche via .key :
if (getkey().key == KEY_EXE) break;
}
Ou avec getkey_opt() :
if (getkey_opt(GETKEY_NONE, NULL).key == KEY_EXE) break;
}
Attention au timeout de getkey_opt(), ce n'est pas un délai mais l'adresse d'une variable. Quand la variable passe à 1, getkey_opt() s'arrête - cela permet d'interrompre l'attente vraiment quand tu le veux. Pour s'arrêter après un délai fixé, il suffit de faire passer la variable à 1 avec un timer.
volatile int timeout = 0;
/* Le timer 2 fera passer la variable à 1 dans 33 ms */
timer_setup(2, timer_delay(2, 33000), timer_Po_4, timer_timeout, &timeout);
/* Si dans 33 ms il ne s'est rien passé, on récupérera un événement KEYEV_NONE */
if (getkey_opt(GETKEY_NONE, &timeout).key == KEY_EXE) break;
}
Je reconnais que c'est pas immédiat... je pourrais mettre un raccourci pour cette construction-là ^^"
Citer : Posté le 11/03/2020 18:20 | #
Merci,
Au fait j'ai une autre question, la première fois que je lance l'add-in, ca marche niquel, mais après l'avoir quitté il m'est impossible de le redémarrer sans avoir au préalable utilisé un autre add-in. Cela semble être présent sur bon nombre d'add-in (même Arena)
J'ai essayé de mettre un petit sleep au début du main, mais cela n'a rien changé ...
A quoi ce problème est-il dû ? Y a t-il une solution pour contourner ce problème ?
Citer : Posté le 11/03/2020 18:50 | #
C'est un problème un peu compliqué. L'OS ne laisse pas les applications se relancer une fois qu'elles sont terminées. Les applications officielles ne se terminent jamais vraiment, elle ne font que revenir temporairement au menu sans quitter. Il y un paramètre pour autoriser le redémarrage des applications après arrêt, mais il n'y a pas de moyen unique de l'activer sur tous les OS. C'est pas trop fait pour.
La façon normale de faire est de revenir au menu pendant un appel à GetKey(). Je pense que tu vois pourquoi ça se complique. Je peux le faire dans gint - c'était implémenté dans une version précédente - et je suis revenu dessus récemment, mais il reste pas mal d'instabilités parce que l'OS fait des choses bizarres pendant qu'on est dans le menu.
Donc il y a des solutions, mais c'est clairement à coder dans gint. C'est tout en haut de ma TODO list gint, il reste juste quelques mystères à résoudre avant que je puisse y parvenir correctement.
Citer : Posté le 12/03/2020 12:06 | #
Bonjour !
La compilation renvoie une erreur. J'ai l'impression que la fonction draw n'arrive pas à utiliser libimg ?
J'ai suivi le tuto, je ne vois pas trop d'où vient l'erreur.
L'image sprite.png se trouve dans assets-cg/img et fait 16x16 pixels.
main.c
draw.c
draw.h
Log
Merci d'avance
Citer : Posté le 12/03/2020 12:55 | #
En passant :
Les angles valides de img_rotate() sont 0, 90, 180 et 270.
(Puisque tu es sur Graph 90+E, tu n'as pas non plus besoin d'utiliser une image intermédiaire, tu peux exécuter le fill() et le rotate() directement sur la VRAM.)
Tu as probablement oublié d'ajouter -limg-cg dans tes flags de compilation, maintenant est-ce que c'est ma faute parce que c'est pas dans le tuto, tout de suite...
Tu as aussi un problème avec tes paramètres de conversion. Par défaut assets-cg/img/sprite.png se fera nommer img_sprite et non pas sprite. Cette convention est malheureuse maintenant que le préfixe img est utilisé pour autre chose. Si tu veux avoir sprite, ajoute IMG.sprite.png = name:sprite dans ton fichier de projet.
(J'ai aussi proposé sur l'issue où on discutait de fxconv une méthode pour pouvoir gérer les paramètres par dossiers, je pense que ça va arriver dans le fxSDK dans le futur.)
Citer : Posté le 12/03/2020 13:46 | #
Merci pour ton aide
J’essaierai tout ça plus tard.
En passant :
Les angles valides de img_rotate() sont 0, 90, 180 et 270.
Ce n'est pas très utile à mon avis, le 0→3 me semble plus logique. Surtout que l'on ne peut pas faire des rotations entre ces valeurs
(Puisque tu es sur Graph 90+E, tu n'as pas non plus besoin d'utiliser une image intermédiaire, tu peux exécuter le fill() et le rotate() directement sur la VRAM.)
Avec img_sub() correct ? Vaut-il mieux utiliser drect() ou img_fill() dans ce cas là ?
Tu as probablement oublié d'ajouter -limg-cg dans tes flags de compilation, maintenant est-ce que c'est ma faute parce que c'est pas dans le tuto, tout de suite...
En effet
Tu as aussi un problème avec tes paramètres de conversion. Par défaut assets-cg/img/sprite.png se fera nommer img_sprite et non pas sprite. Cette convention est malheureuse maintenant que le préfixe img est utilisé pour autre chose. Si tu veux avoir sprite, ajoute IMG.sprite.png = name:sprite dans ton fichier de projet.
(J'ai aussi proposé sur l'issue où on discutait de fxconv une méthode pour pouvoir gérer les paramètres par dossiers, je pense que ça va arriver dans le fxSDK dans le futur.)
Ce n'était pas dans le tuto non plus
Le img_ ne me dérange pas pour le coup.
Je vais voir l'issue
Merci !
Citer : Posté le 12/03/2020 13:59 | #
C'est marrant les débats sur les APIs. Je ne suis pas d'accord pour pas mal de raisons, même si c'est subjectif après un certain point.
• Ce n'est pas vraiment lisible. rotate(90) on voit tout de suite ce que ça fait, rotate(3) tu lis le header une fois sur deux pour t'en rappeler. C'est un peu comme quand j'écris dsubimage(my_image, 0, 0, 10, 10, 0). Tu ne vois pas du tout ce que fait le dernier 0 sauf si je l'écris DIMAGE_NONE auquel cas tu comprends que c'est les options de rendu (clipping).
• Si tu calcules ton angle tu n'es pas à un *90 près dans ton code.
• Utiliser des multiples de 90 rend très explicite le fait que les angles sont en degrés, ce qui est pas la norme dans la libm (qui utilise des radians).
• Et enfin, c'est le seul choix qui sera toujours correct le jour où quelqu'un codera des rotations arbitraires.
Pour remplir ce serait avec img_sub() en effet parce qu'il ne faut pas aller jusqu'en bas à droite de la VRAM :
Mais oui drect() est mieux ici. Pour la rotation, comme le résultat ne tient pas compte de la taille de l'image passée en paramètre (il est juste produit dans le coin haut gauche), tu peux utiliser img_at() :
Zut, j'y avais pensé pourtant. Je vais le rajouter ce soir.
Ça va te gêner assez vite si tu as le malheur d'appeler une image fill.png, rotate.png, vram.png, etc, y compris si tu la convertis pour bopti. Une erreur cryptique de linker va te tomber dessus.
Citer : Posté le 13/03/2020 08:57 | #
OK, je comprend plus vite le 0, 1, 2, 3 (c'est une simple rotation), mais tes points se valent
Le code compile et fonctionne avec les changements donnés, mais le return 0; de main ne retourne pas au menu.
main.c
draw.h
draw.c
Merci d'avance
J'ai une suggestion à faire, je vais sur le topic de libimg.
Citer : Posté le 13/03/2020 12:11 | #
Hmm, ton combo avec clearevents() et keydown() est curieux. De façon générale, il faut les mettre ensemble. En dormant entre les deux, tu prends le risque que la pression de touche passe inaperçue. (C'est presque certain que le problème n'a rien à voir avec le return et que tu ne sors juste jamais de la boucle principale.) Enfin dans tous les cas, il est peu probable qu'on tape à 60 FPS donc ça devrait détecter.
Je testerai, laisse-moi un petit moment...
Quelques notes au passage :
• Tu devrais obtenir la déclaration de dupdate() par <gint/display.h>
• Le callback que tu as défini est courant, donc il est implémenté dans gint sous le nom timer_timeout()
Citer : Posté le 14/03/2020 10:44 | # | Fichier joint
Merci pour ta réponse.
Le dupdate() est un oubli, il n'est même pas utilisé dans main
Merci pour ce callback ! J'en avais un peu marre de le définir à chaque fois ^^'
Quand au clearevents() et keydown(), d'après ma logique (et de ce que j'ai compris du système) voici le graphe obtenu :
Je ne vois pas vraiment le problème, les entrées sont clear avant l'attente. Je pensais que justement cela réduirait la perte d'informations
Ajouté le 14/03/2020 à 11:15 :
J'ajoute que le return 0 termine bien le programme, l'image arrête de tourner. Mais pas de retour au menu.
Citer : Posté le 14/03/2020 11:31 | #
Hmm... non, c'est pas cet ordre-là que tu as envie d'utiliser. Je suis pas encore sûr de pourquoi ton bug se produit, je voulais tester après avoir écrit ce message mais ça m'a pris un moment donc je vais devoir repousser un peu. Néanmoins j'espère pouvoir expliquer pourquoi keydown() est toujours un mauvais choix, et surtout l'alternative supérieure qui tient en deux lignes de plus.
La façon dont les événements fonctionne est la suivante : un timer scanne le clavier et enfile les événements « instantanément » [1] dans la file d'événements. L'add-in peut accéder à la file d'événements pour savoir ce qu'il s'est passé récemment, typiquement entre deux frames d'un jeu. Si le jeu lagge beaucoup, pas mal d'événements peuvent s'être glissés entre les deux frames donc le jeu devra les défiler et les rejouer dans l'ordre.
Dans le modèle événementiel pur, tous les événements sont lus avec pollevent() ou waitevent(), qui retournent tous les deux le premier événement en attente de la file (donc le plus ancien non traité), avec des paramètres d'attente différents si la file est vide. Et ton programme ressemblerait à ça (l'ordre n'est pas obligatoire) :
Unqueue pending events; if KEY_EXIT was pressed since last frame, break.
Update and draw.
Wait until 1/64s have elapsed since start of frame.
Là, tu prends zéro risque parce tu es sûr que tu vas lire tous les événements et que tu ne vas pas rater une pression de KEY_EXIT [2].
Pour comprendre keydown(), il faut voir qu'il y a deux états du clavier dans l'add-in :
• L'état physique avec les touches pressées à chaque instant par le joueur (connu uniquement par le driver clavier) ;
• L'état apparent pour l'application, qui est en retard sur l'état physique parce que les derniers changements sont encore dans la file d'événement et l'application n'en a pas encore pris connaissance.
La clé qui permet d'intégrer de keydown() au système d'événements et de le faire interagir élégamment avec getkey() est de le faire travailler sur l'état apparent uniquement. Quand tu demandes keydown(KEY_EXIT), ça ne regarde pas si la touche est pressée en cet instant, ça regarde si elle était pressée au moment où s'est produit le dernier événement que tu as lu. Je digresserais si j'essayais d'expliquer pourquoi c'est la bonne façon de faire ici, mais tu comprendras je pense que puisque getkey() utilise les événements et n'observe donc que l'état apparent, si keydown() se met à observer un autre état il y a plein de problèmes de désynchronisation qui se posent.
Pour obtenir l'effet désiré de keydown() (d'observer l'état physique), il suffit de lire tous les événements pour que l'état apparent rattrape l'état physique en vidant la file. D'où le combo clearevents() suivi de keydown() qui est mentionné dans la doc. Tu comprends que clearevents() doit être juste avant keydown(), sinon on n'atteint pas l'effet désiré d'observer l'état physique du clavier. Dans tous les cas, en faisant ça on jette les contenus de la file, et tu te doutes que supprimer de l'information va poser des problèmes. C'est le défaut fondamental de keydown() : keydown() est une fonction intrinsèquement impure car elle ignore les problématiques de timing. [3]
Si la file contient deux événements : KEY_EXIT pressé et KEY_EXIT relâché, keydown(KEY_EXIT) renvoie 0 (car la pression n'a pas encore été observée) et après avoir vidé la file elle renvoie encore 0 (car le relâchement a été observé). Ça revient au même que d'avoir fait IsKeyDown() entre deux tours de boucle du SDK si EXIT a été pressée et relâchée très vite pendant le même tour. Excepté que là l'information existait dans la file d'événements et qu'on l'a juste royalement ignorée ! Et ça, tu ne peux rien y faire. keydown() aura toujours ce défaut majeur.
Venons-en donc au problème du placement de clearevents() dans ta boucle. Compte tenu de l'effet de la boucle, tu fais clearevents() juste après avoir utilisé keydown() (au lieu de juste avant). C'est le pire placement possible, car chaque keydown() va se référer à l'état apparent issu du clearevents() qui s'est produit au tour précédent... 1 FPS plus tôt. Donc tes entrées sont toutes traitées avec un frame de retard ! Ta version du programme auraît du ressembler à ça :
Clear events.
If KEY_EXIT is pressed, break.
Update and draw.
Wait until 1/64s have elapsed since start of frame.
Et là tu vois la différence entre les deux versions :
• Dans la première, on regarde si une pression de KEY_EXIT a eu lieu depuis le dernier frame.
• Dans la seconde, on ignore le déroulement progressif des événements et on espère vaguement que si l'utilisateur a pressé KEY_EXIT alors il ne l'a pas encore relâché.
Ça devrait être évident que la méthode avec keydown() est bancale. Quant à la première, elle se code en quatre lignes :
while((ev = pollevent()).type != KEYEV_NONE)
{
if(ev.type == KEYEV_DOWN && ev.key == KEY_EXIT) game_loop = 0;
}
La condition (un peu expéditive) de la boucle récupère le plus ancien événement non traité dans ev et s'arrête si on arrive au bout de la file (ce qui est signalé par un événement « vide » de type KEYEV_NONE). Et dans la boucle, on arrête le jeu si une pression de KEY_EXIT est détectée.
C'est plus long. C'est aussi plus correct. Aucun jeu qui traite ses entrées sérieusement ne devrait utiliser keydown() et encore moins clearevents(). Et s'imposer de tourner à 60 FPS démontre un souci du timing bien trop rigoureux pour se permettre de faire ce genre d'approximations.
Bon encore une fois, je sais pas si c'est ça ton bug (60 FPS reste assez rapide pour que tu puisses attraper la pression avec ce système un peu bancal en principe). J'espère ne pas faire le vieux con qui donne des leçons en passant à côté du problème. Je testerai pour de bon cet après-midi, promis.
[1] Techniquement, à 128 Hz.
[2] Sauf si le programme est vraiment hyyyper lent et que la file se retrouve pleine pendant suffisamment longtemps - ça n'arrivera jamais.
[3] Lire le clavier à 128 Hz ne fait que repousser le problème ultimement, mais ça c'est un problème de gint. Avec un vrai driver KEYSC comme celui rétro-analysé par Yatis, on peut se faire notifier des changements sur le clavier et n'en rater réellement aucun. Pour ne pas se perdre dans les détails du développeur de driver, considérons que gint ne rate jamais rien de ce qui se passe sur le clavier.
Citer : Posté le 14/03/2020 11:48 | #
au bah, quel hasard, ce dont j'avais besoin pour mon emulateur chip8 ♥️
Citer : Posté le 15/03/2020 12:23 | #
Content que ça soit utile ! Je pense que ça partira dans un tutoriel d'utilisation de gint quand j'en serai arrivé là.
Citer : Posté le 15/03/2020 13:11 | #
Bonjour,
Je cherches une police d'écriture 5x3 interfacable avec gint ... J'ai bien trouvé la police modern de darkstorm mais elle n'est pas au bon format il me semble, du coup j'ai une erreur à chaque fois que je veux l'utiliser.
Du coup si quelqu'un a déjà vu ça trainer Sur planet Casio je suis preneur
Citer : Posté le 15/03/2020 13:13 | #
Le format n'est pas difficile à obtenir. Je te conseille de prendre ce que tu as trouvé et on peut l'adapter ensemble.