Zlib pour Casio fx/cg (développement et benchmark)
Posté le 01/05/2022 11:20
Hello à Toutes et Tous
Dans le long voyage qui nous amènera peut-être (enfin j'espère bien) à avoir une librairie SDL complète (avec SDL_image), il y a un certain nombre de prérogatives à passer, notamment pour le support des ressources externes en format PNG. En effet la SDL_image s'appuie sur la libPNG, qui elle même repose sur la zlib. Donc il faut prendre les choses les unes après les autres et je vous ai créé un dépôt avec la zlib fonctionnelle pour les Casio (fx/cg).
La zlib, mais kézako ? Pour ceux qui ne connaissent pas, il s'agit d'une bibliotheque de fonctions visant à offrir des algorithmes de compression / décompression sans perte de données. Cette bibliothèque est très largement utilisée par de nombreux logiciels (sur à peu près toutes les plateformes existantes), y compris par l'OS de nos Casio chéries. En effet, l'OS contient quelques syscall faisant référence à cette zlib, mais hélas à ce stade nous ne savons pas les utiliser car peu ou prou documentés.
Donc notre zlib opérationnelle se nomme cZlib (contraction de "Casio Zlib") et repose sur la zlib version 1.2.5. Le dépôt Gitea de la bête se trouve ici :
cZlib1.2.5
Comme d'habitude il est possible d'installer la libczlib via GiteaPC par la commande suivante
giteapc install Slyvtt/cZlib1_2_5
la compilation se faisant à la volée ainsi que l'installation dans les répertoires adhoc du compilateur contenant les headers et les librairies. La encore, si vous avez des soucis, passer sur fxlibc@dev, car il se peut que certaines features récentes soit nécessaires et pas encore dans la branche master.
Pour les plus aventuriers, il y a aussi un Makefile à lancer dans le répertoire racine d'extraction via un
make -f Makefile.prizm
Il vous faudra alors copier manuellement libczlib.a dans le répertoire d'installation de vos lib de votre compilateur et les deux fichiers headers zlib.h et zconf.h contenus dans le répertoire racine dans le sous dossier include de votre compilateur.
Là encore comme d'hab', vous pourrez commencer à créer un programme avec la zlib très simplement.
Créer un nouveau projet via
fxsdk new SDLproject
puis vous devrez ouvrir le fichier
CMakeLists.txt contenu dans le projet pour éditer la ligne
target_link_libraries(myaddin Gint::Gint)
et la remplacer par
target_link_libraries(myaddin Gint::Gint -lczlib)
(bref ajouter la librairie qui va bien).
voici un code exemple pour utiliser :
#include <gint/gint.h>
#include <gint/display.h>
#include <stdio.h>
#include <string.h> // for strlen
#include <assert.h>
#include "zlib.h"
int main()
{
// original string to be compressed
char a[50] = "Hello Hello Hello Hello Hello Hello !!!!!!";
// placeholder for the compressed (deflated) version of "a"
char b[50];
// placeholder for the UNcompressed (inflated) version of "b"
char c[50];
dclear( 0xFFFF );
dprint(1,10,0x0000, "Uncompressed size is: %lu", strlen(a));
dprint(1,20,0x0000, "Uncompressed string is:");
dprint(1,30,C_BLUE, "%s", a);
// STEP 1 : deflate a into b. (that is, compress a into b)
// zlib struct
z_stream defstream;
defstream.zalloc = Z_NULL;
defstream.zfree = Z_NULL;
defstream.opaque = Z_NULL;
// setup "a" as the input and "b" as the compressed output
defstream.avail_in = (uInt)strlen(a)+1; // size of input, string + terminator
defstream.next_in = (Bytef *)a; // input char array
defstream.avail_out = (uInt)sizeof(b); // size of output
defstream.next_out = (Bytef *)b; // output char array
// the actual compression work.
deflateInit(&defstream, Z_BEST_COMPRESSION);
deflate(&defstream, Z_FINISH);
deflateEnd(&defstream);
dprint(1,50,0x0000, "Compressed size is: %lu", strlen(b));
dprint(1,60,0x0000, "Compressed string is:" );
dprint(1,70, C_BLUE, "%s", b);
dprint(1,80,C_RED, "Don't worry if not all char are visible :");
dprint(1,90,C_RED, "may contain non printable ones" );
// STEP 2 : inflate b into c (should return to string a)
// zlib struct
z_stream infstream;
infstream.zalloc = Z_NULL;
infstream.zfree = Z_NULL;
infstream.opaque = Z_NULL;
// setup "b" as the input and "c" as the compressed output
infstream.avail_in = (uInt)((char*)defstream.next_out - b); // size of input
infstream.next_in = (Bytef *)b; // input char array
infstream.avail_out = (uInt)sizeof(c); // size of output
infstream.next_out = (Bytef *)c; // output char array
// the actual DE-compression work.
inflateInit(&infstream);
inflate(&infstream, Z_NO_FLUSH);
inflateEnd(&infstream);
dprint(1,110,0x0000, "Uncompressed size is: %lu", strlen(c));
dprint(1,120,0x0000, "Uncompressed string is:" );
dprint(1,130,C_BLUE, "%s", c);
// make sure uncompressed is exactly equal to original.
if(strcmp(a,c)==0) dprint(1,150, C_GREEN, "Everything is OK" );
else dprint(1,150, C_RED, "There is a problem somewhere" );
dupdate();
getkey();
return 0;
}
Et voici deux screenshots pris sur ma G90+E pour montrer que ça marche :
Fun fact : vous noterez l'efficacité de la compression dans le premier cas
Cela pourra toujours vous servir pour des utilitaires à un moment ou à un autre.
Attention, comme d’habitude avec les flux dans les fichiers : bien penser au
gint_world_switch qui va bien !!!
Afin de mesurer les performances de la cZlib sur nos machines, j'ai testé différentes configurations sur divers fichiers. Le protocole de test est le suivant :
- ouverture du fichier et stockage de l'ensemble des données contenues dans celui-ci en mémoire dans un flux (stream au sens de la zlib) d'entrée ("inputStream").
- compression de ce flux d'entrée via la fonction "compress()" de la zlib et stockage dans un flux intermédiaire ("outputStrean") avec mesure du temps de compression et la taille du flux compressé / du ratio de compression.
- décompression du flux intermédiaire vers le flux final ("secondOutputStream") via la fonction "uncompress()" de la zlib, là encore avec mesure du temps de décompression et la taille du flux décompressé (qui doit retomber sur la taille initiale, sinon il y a un problème).
- validation octet par octet de la similitude de "inputStream" et de "secondOutputStream" (on regarde que le cycle compression/décompression est sans erreur).
- écriture sur disque du flux decompressé finale pour vérifier que l'on peut utiliser le fichier sans soucis (a priori redondant avec étape précédente, mais on est jamais assez prudent
).
A ce stade les tests sont réalisés sur une fx-CG 50 (ou Graph 90+E) sans overclock. Je pense complèter par d'autres essais plus tard. Les temps sont mesurés avec la LibProf.
Les fichiers tests sont :
-
3 fichiers Addins (des ".g3a") de diverses tailles : Conv.g3a (~29ko) et PicPlot.g3a (~83ko) de Casio et Afterburner de Lephé (~172ko)
-
1 fichier texte (".txt") créé en ligne par un générateur de phrases aléatoires en français : texte0.txt (~36ko)
-
3 fichiers images (".bmp") sans compression : image0.bmp (~873ko) photo 660x441pix en 24bpp, image1.bmp (~265ko) image de fond de mon jeu OutRun 396x224pix en 24bpp, et image2.bmp (~57ko) convertie depuis une fonte fxconv, image en N&B,
-
3 fichiers photos (".png") avec compression : photo0.png (~26ko), photo1.png (~194ko), photo3.png (~579ko)
Et sans plus attendre voici les résultats obtenus :
Il est intéressant de voir les niveaux de compression et surtout les temps de décompression qui sont vraiment pas mauvais du tout et pourrait laisser envisager quelques trucs sympas dans Gint.
Code utilisé (Cliquer pour dérouler)
Code utilisé (Cliquer pour enrouler)
#include <gint/gint.h>
#include <gint/display.h>
#include <gint/keyboard.h>
#include <gint/hardware.h>
#include <gint/kmalloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // for strlen
#include <assert.h>
#include <sys/stat.h>
#include "zlib.h"
#include <libprof.h>
char *filesin[10] = { "photo0.png", "photo1.png", "photo2.png", "Conv.g3a", "PictPlot.g3a", "Afterbur.g3a", "texte0.txt", "image0.bmp", "image1.bmp", "image2.bmp" };
char *filesout[10] = { "Z_photo0.png", "Z_photo1.png", "Z_photo2.png", "Z_Conv.g3a", "Z_PictPlot.g3a", "Z_Afterbur.g3a", "Z_texte0.txt", "Z_image0.bmp", "Z_image1.bmp", "Z_image2.bmp" };
struct stat sb;
uint32_t time_compress=0, time_decompress=0;
uint32_t sizeStream;
char *inputStream = NULL;
char *outputStream = NULL;
char *secondOutputStream = NULL;
int OpenFileAndLoadStream( int which )
{
if (inputStream!=NULL) {
free( inputStream );
inputStream = NULL;
}
if (outputStream!=NULL) {
free(outputStream );
outputStream = NULL;
}
if (secondOutputStream!=NULL) {
free( secondOutputStream );
secondOutputStream = NULL;
}
if (stat(filesin[which], &sb) == -1) {
return -1;
}
sizeStream = (uint32_t) sb.st_size;
inputStream = (char*) malloc( sb.st_size );
if (inputStream == NULL) {
return -2;
}
outputStream = (char*) malloc( sb.st_size+10000 );
if (outputStream == NULL)
{
free(inputStream);
inputStream = NULL;
return -3;
}
secondOutputStream = (char*) malloc( sb.st_size );
if (secondOutputStream == NULL)
{
free(outputStream);
inputStream = NULL;
free(outputStream);
inputStream = NULL;
return -4;
}
FILE* fp = fopen(filesin[which], "rb" );
if (fp==NULL) {
return -5;
}
fread( inputStream, sb.st_size, 1, fp );
fclose ( fp );
return 1;
}
int SaveOutputFile( int which )
{
FILE* fp = fopen(filesout[which], "wb" );
if (fp==NULL) {
return -5;
}
fwrite( secondOutputStream, sizeStream, 1, fp );
fclose ( fp );
return 1;
}
void CloseAndFree( void )
{
if (inputStream!=NULL) {
free( inputStream );
inputStream = NULL;
}
if (outputStream!=NULL) {
free( outputStream );
outputStream = NULL;
}
if (secondOutputStream!=NULL) {
free( secondOutputStream );
secondOutputStream = NULL;
}
exit(0);
}
bool canWeAllocate3Mb = false;
static kmalloc_arena_t extended_ram = { 0 };
void increaseRAM( void )
{
char const *osv = (char*) 0x80020020;
//kmalloc_gint_stats_t *extram_stats;
if((!strncmp(osv, "03.", 3) && osv[3] <= '6') && gint[HWCALC] == HWCALC_FXCG50) // CG-50
{
extended_ram.name = "extram";
extended_ram.is_default = true;
extended_ram.start = (void *)0x8c200000;
extended_ram.end = (void *)0x8c500000 ;
kmalloc_init_arena(&extended_ram, true);
kmalloc_add_arena(&extended_ram );
canWeAllocate3Mb = true;
}
else if (gint[HWCALC] == HWCALC_PRIZM) // CG-10/20
{
extended_ram.name = "extram";
extended_ram.is_default = true;
uint16_t *vram1, *vram2;
dgetvram(&vram1, &vram2);
dsetvram(vram1, vram1);
extended_ram.start = vram2;
extended_ram.end = (char *)vram2 + 396*224*2;
kmalloc_init_arena(&extended_ram, true);
kmalloc_add_arena(&extended_ram );
canWeAllocate3Mb = false;
}
else if (gint[HWCALC] == HWCALC_FXCG_MANAGER) // CG-50 EMULATOR
{
extended_ram.name = "extram";
extended_ram.is_default = true;
extended_ram.start = (void *)0x88200000;
extended_ram.end = (void *)0x88500000 ;
kmalloc_init_arena(&extended_ram, true);
kmalloc_add_arena(&extended_ram );
canWeAllocate3Mb = true;
}
else abort();
}
int main()
{
uint16_t *vram1, *vram2;
dgetvram(&vram1, &vram2);
dsetvram(vram1, vram1);
increaseRAM();
prof_init();
prof_t perf_compress, perf_decompress;
int file=9;
int retour = (int) gint_world_switch( GINT_CALL(OpenFileAndLoadStream, file) );
if (retour<=0) CloseAndFree();
uint32_t ucompSize = sizeStream; // "Hello, world!" + NULL delimiter.
uint32_t compSize = compressBound(ucompSize);
uint32_t reucompSize = sizeStream; // "Hello, world!" + NULL delimiter.
perf_compress = prof_make();
prof_enter(perf_compress);
// Deflate
compress((Bytef *)outputStream, &compSize, (Bytef *)inputStream, ucompSize);
prof_leave(perf_compress);
time_compress = prof_time(perf_compress);
perf_decompress = prof_make();
prof_enter(perf_decompress);
// Inflate
uncompress((Bytef *)secondOutputStream, &reucompSize, (Bytef *)outputStream, compSize);
prof_leave(perf_decompress);
time_decompress = prof_time(perf_decompress);
dclear( 0xFFFF );
dprint(1,10,0x0000, "Uncompressed size is: %lu", ucompSize );
dprint(1,20,0x0000, "Compressed size is: %lu", compSize );
dprint(1,30,0x0000, "Uncompressed size is: %lu", reucompSize );
dprint(1,40, C_GREEN, "time to compress: %lu", time_compress/1000 );
dprint(1,50, C_GREEN, "time to decompress: %lu", time_decompress/1000 );
dupdate();
for( uint32_t k=0; k<sizeStream; k++ )
{
if (inputStream[k]!=secondOutputStream[k])
{
dprint(1,70,C_RED, "Error at byte %d", k );
break;
}
}
dprint(1,90,C_BLUE, "End of comparison" );
dprint(1,100, 0x0000, "Now Saving Ouput ... " );
dupdate();
retour = (int) gint_world_switch( GINT_CALL(SaveOutputFile, file ) );
dprint(1,110, 0x0000, " ... Done :-) " );
dprint(1,130, 0x0000, "Press a key to exit" );
dupdate();
getkey();
// We set back zeros at the end of the program
memset(extended_ram.start, 0, (char *)extended_ram.end - (char *)extended_ram.start);
return 0;
}
Ciao
Sly
Et zou, @RDP
Citer : Posté le 02/05/2022 16:52 | #
Yo, alors je suis assez intéressé par les taux personnellement.
J'ai réfléchi à la question de compresser les add-ins. Après tout, il y a pas mal de code, alors autant en profiter pour réduire les temps de transfert. Le problème crucial c'est que l'OS n'allouera des pages que jusqu'à la taille du fichiers g3a, ce qui veut dire en termes pratiques que on ne peut pas compresser ce qui doit vivre dans la ROM à l'exécution.
En particulier si on compresse des images (la toute première cible évidemment), il faudra les décompresser dans la RAM, ce qui retire la possibilité de s'appuyer sur les 2 Mo de l'add-in pour stocker des choses. Si on s'autorise l'accès à la RAM au-delà des 512 ko et jusqu'à 3/6 Mo ça ne devrait pas être un problème, cela dit.
Cette approche permettrait aussi de décompresser à la demande, par exemple d'avoir en RAM les données du niveau courant uniquement.
Pour ce qui est du format, le plus évident de loin est de compresser les tableaux de pixels des images bopti. Je vois pas trop de raison de s'embêter avec du PNG sauf pour porter des programmes qui s'en servent déjà. En tous cas la réduction potentielle de taille des add-ins est assez intéressante, surtout en termes de temps de transfert.
Citer : Posté le 02/05/2022 19:49 | #
juste j'ai oublié de préciser que le compression ratio est en % et qu'il est calculé par la formule suivante :
Cr% = (taille_initiale - taille_apres_compression) / taille_initiale * 100
Logique, mais mérite d'être explicité clairement.
Citer : Posté le 05/05/2022 09:38 | # | Fichier joint
Petite update sur l'utilisation de la Zlib sur Prizm/Graph 90+E.
Grosso modo le moteur est fonctionnel et la lib est utilisable, mais il y a des cas plus exotiques qu'il faut traiter et/ou tester.
En ce moment, je suis en train de regarder pour compresser de gros fichiers en utilisant un "flux" glissant en entrée et en sortie, mais pour le moment j'ai quelques problèmes de crash.
L'idée derrière cela serait d'avoir une allocation mémoire minimale (de seulement quelques Ko correspondant à un "chunk") qui serait mis à jour dans la routine de compression et/ou de décompression jusqu'à ce que le fichier complet soit compressé ou décompressé. Actuellement je charge l'intégralité du contenu du fichier à traiter en RAM donc il y a de vraiment grosses allocations de mémoires nécessaires.
Cela ne pose en soit pas de problème sur la G90+E car on a de la marge en RAM en utilisant les ressources "non officielles", mais peut s'avérer déjà plus complexe sur les prizm (CG10/20) et très très complexe sur les monochrome a priori. Donc cela limiterait la taille des fichiers manipulables, faute de pouvoir les charger complètement en RAM pour opérer dessus par la suite.
J'espère donc pouvoir vous apporter quelques bonnes nouvelles rapidement avec un fichier exemple de l'utilisation de la lib (car c'est pas forcément très très limpide je reconnais ).
Sly
@RDP