MSX Village forum

La Place des Développeurs Projet GOS Et oui, encore trop d'ambitions ^^

ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 23/12/2020 à 07h07

Reprise du message précédent

Bien joué ! :) :top

Mais heu... j’ai beaucoup de mal avec ton code... :oups
Peux tu expliquer cette technique ici :D


banniere-ericb59e
Site web    
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2904

Le 23/12/2020 à 14h09
(@Eric, je me permets qq rappels de base au cas où d’autres seraient intéressés)

Accrochez vos ceintures, c’est parti !

Donc, le cœur du problème c’est la fonction qui décode le dessin d’un caractère pour le Screen mode 8, et l’envoi en VRAM pour l’afficher à l’écran ( PutChar_G7 dans ma lib).
Par défaut, les dessins d’un caractère font 8 points de large sur 8 points de haut et sont stockés sous forme binaire (1 : plein, 0 : transparent). Le dessin d’un caractère est donc composé de 8 octets représentant chaque ligne, et chacun des 8 bits de ces octets représentant 1 point dans la largeur.
Le Screen mode 8, lui, stock les images sous la forme : 1 point = 1 octet (les 256 valeurs possibles représentent la couleur du point au format Vert-Rouge-Bleu)
Le but de la fonction PutChar_G7() est donc de transformer chaque bit à 1 dans le dessin du caractère en un octet contenant la couleur du texte à afficher, et chaque bit à 0 en un octet contenant la couleur de fond.

Approche basique du problème :

Code TEXT :
- Pour un caractère donné (prenons 'A' comme ex.) :
  - Trouver l’adresse de son dessin (le caractère 'A' a le code ASCII 65, son dessin est donc le 65-ième dans le tableau)
  - Pour chaque ligne du dessin (Y allant de 0 à 7) :
    - Récupérer l’octet de la ligne correspondant (L)
    - Pour chaque colonne du dessin (X allant de 0 à 7) :
      - Déterminer l’adresse où envoyer les données en VRAM (en fonction de la position où afficher 'A')
      - Si le X-ième bit de L vaut 1, envoyer l’octet de la couleur du texte en VRAM
      - Si le X-ième bit de L vaut 0, envoyer l’octet de la couleur de fond en VRAM (ou ne rien faire pour la transparence)


Ce qui donne en C :
Code C :
void PutChar_G7(u8 chr)
{
    const u8* form = g_PrintData.FontAddr + chr * 8; // character form address
    u16 addr = (g_PrintData.CursorY * 256) + g_PrintData.CursorX; // vram destination address
    for(i8 y = 0; y < 8; y++) // lines
    {
        for(i8 x = 0; x < 8; x++) // columns
        {
            if(form[y] & (1 << (7 – x))) // check if the x-th bit is 0
                g_PrintData.Buffer[i] = g_PrintData.TextColor;
            else
                g_PrintData.Buffer[i] = g_PrintData.BackgroundColor;
        }
        VDP_WriteVRAM(g_PrintData.Buffer, addr, 0, 8); // draw 8-bytes
        addr += 256;
    }
}


Cela fonctionne, mais c’est très lent pour plusieurs raisons :
- Beaucoup de calculs à faire pour chaque point du dessin du caractère
- Les boucles imbriquées coûtent cher en C (et même en assembleur)
- Envoyer les données en VRAM une-à-une n’est pas optimal (en fait, dans le cas présent, le CPU passe tellement de temps à décoder le dessin du caractère que les temps d’écriture dans la VRAM ne sont pas vraiment problématiques).

L’idée est donc de :
- Pré-calculer tout ce qui peut l’être
- Dérouler la deuxième boucle (celle qui passe en revue chaque bit d’une ligne)

Pour les pré-calculs, je vais pas rentrer dans le détail car c’est assez simple : toutes les valeurs qui ne dépendent pas du caractère à afficher devrait être calculé une seule fois qq part (dans une fonction d’initialisation par ex.) et stockées en RAM.

Là ou ça devient amusant, c’est le "déroulage" de la deuxième boucle.
Une approche basique serait de remplacer la boucle de 8 itérations par 8 tests consécutifs avec des valeurs de bit fixes (0, 1, 2, … 7).
C’est déjà mieux car on élimine la boucle imbriquée et les valeurs fixes sont bien mieux optimisées par le compilateur... mais on peut faire encore beaucoup mieux. ;)

Au lieu de prendre les bits d’une ligne 1 par 1, prenons les 2 à 2.
Déjà, on va se retrouver avec 4 traitements à faire au lieu de 8 (ce qui est déjà mieux).
Mais surtout, avec 2 bits, on a 4 combinaisons possibles ([0,0], [0,1], [1,0], [1,1]) qu’on va pouvoir utiliser comme des valeurs de 0 à 3.
En utilisant ces valeurs comme index pour récupérer les données de couleur dans un tableau pré-rempli, on élimine tous les tests conditionnels et on réduit fortement les calculs.
Le tableau (à ré-initialiser dès qu’on change la couleur du texte) contient 4 valeurs de 16-bits représentants les 2 couleurs consécutives des 4 combinaisons possibles :
Code C :
tab[0] = (bgColor << 8) + bgColor ;     // [0,0]
tab[1] = (textColor << 8) + bgColor ;   // [0,1]
tab[2] = (bgColor << 8) + textColor ;   // [1,0]
tab[3] = (textColor << 8) + textColor ; // [1,1]


Et voilà !
Maintenant, on a une fonction 4~5 fois plus rapide.
Le léger surcoût sur la fonction de changement de couleur est largement compensé par le gain sur l’affichage d’un caractère (surtout qu’il est rare qu’on change de couleur très souvent dans un texte).

On peut aller encore plus loin si on veut :
- Y a plein de petits détails à optimiser
- La font (police de caractère) contenu dans la Main-ROM ayant une taille de 6 points de large sur 8 points de haut, on a besoin que de 3 traitements de 2-bits par ligne.
- On pourrait aussi traiter les lignes par 3 bits (dans ce cas, il faut précalculer et stocker 8 valeurs de 24-bits)
- On pourrait aussi le faire par 4 bits (16 valeurs de 32-bits… ce qui commence à faire beaucoup)

Voilà, j'espère que c'est plus clair. :)


On est toujours ignorant avant de savoir.
Github    
Franck Membre non connecté

Maire-adjoint

Rang

Avatar

Association

Inscrit le : 02/10/2009 à 22h54

Messages: 3345

Le 23/12/2020 à 16h47
Très intéressant tout ça, même si j'avoue que je ne suis pas totalement tout :oups

Faites des GOS ^^ :jesors
   
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 23/12/2020 à 20h42
Alors j'ai bossé la dessus une bonne partie de la journée l'air de rien !
J'en ai mal aux yeux :|

Avec une méthode "traditionnelle" d'analyse des bits, j'étais tombé à 48 Cycles (Screen8, 50Hz).

Ton explication sur la méthode de scinder l'octet en 4 pairs de bits est très claire, et d'y associer un tableau de couleurs, c'est très malin en effet.

J'ai voulu faire ça à ma manière, mais je me suis cassé les dents pour faire rentrer des "INT" dans un tableau de "Char".
Hors de question d'utiliser un "Itoa" qui ralentirait le bouzin...

Alors j'ai repomper ton code :lol
Et utilisé les pointeurs comme tu le fais. Les pointeurs c'est quelques chose que j'utilise de façon basique, et là, je dois dire, que je ne pige pas la logique, et la façon dont ca fonctionne. Mais ça le fait ... donc ... :gne

Alors maintenant je suis à 32 Cycles.

Le traitement des données prend 16 Cycles, la copie en VRAM 16 Cycles.
Je ne sais pas comment tu fais pour arriver encore plus bas ! :hum





banniere-ericb59e
Site web    
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2904

Le 23/12/2020 à 21h21
Passer de 32 à 19 frames, ça va se jouer dans les détails... et là c'est très compliqué de te dire quoi changer sans avoir un outil de profiling (qui nous dirait où le CPU passe son temps).
Y a des optims globales : utiliser les FastCall, des variables uint8, éviter les boucles imbriquées, pre-incrémenter, dérouler les otir, etc.
Chaque ligne de code que j'écris est souvent pensé pour être la plus rapide possible.

Y a aussi un truc un peu tricky que j'ai choisi de garder mais qui risque de pas te plaire ^^, c'est que je n'attends pas la libération du VDP avant d'envoyer mes commandes pour les prints.
J'ai calculé que le temps de traitement CPU est supérieur au temps d'envoie des données en VRAM donc pas besoin de check l'état du VDP entre les envois de caractère.
Par contre, j'avais un bug quand j'exécutais une grosse commande VDP juste avant un Print (le 1er caractère n'apparaissait pas entièrement).
Dans cas, je demande à l'utilisateur (moi pour le moment ^^) d'ajouter un VDP_WaitReady() à la main pour corriger le problème.
On pourrait l'ajouter au début du Print comme ça l'attente se fait par chaine de caractère et pas par caractère, mais perso je privilégie la rapidité et la flexibilité à la sécurité. :p

Tu es passé de 197 frames à 32... c'est déjà pas mal. :top


On est toujours ignorant avant de savoir.
Github    
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2904

Le 24/12/2020 à 00h15
Pour info j'ai rajouté le support du Screen 7 (512x212 en 2 bits-par-couleur).
C'est encore plus rapide (14 frames) car je gère les points 4 à 4 (pour manipuler des octets).
Par contre, il faut 16 bytes de données précalcl.
C'est commit sur GitHub si tu veux récupérer ça. ;)

Après, même 14 frames pour afficher 7 lignes, ça reste extrêmement lent.
Encore une fois, la vraie solution serait de décoder les fonts en RAM ou en VRAM et de ne faire que des copies.


On est toujours ignorant avant de savoir.
Github    
TurboSEB Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 08/08/2010 à 20h57

Messages: 5886

Le 24/12/2020 à 03h57
C'est dingue ça, j'ai tout compris, enfin, dans le principe disont, même si je suis incapable synthétiser la chose, car j'ai déjà oublié le début :oups c'est effectivement très intéressant :top
Bon si je comprends bien, ce "rafraîchissement bête et méchant" systématique de l'écran serait encore plus efficace si l'on pouvait optimiser le travail sur seulement la partie qui a changée, ça doit être predictible non !? ^^ Genre si c'est cette position là, on rafraîchi seulement cette ligne la :) Edité par TurboSEB Le 24/12/2020 à 03h59



MSX 1&2 + Moniteurs+divers (environ 0.70Tonnes)
   
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 24/12/2020 à 06h58
@TurboSeb Malheureusement on pas de contrôle là dessus sur MSX

@aoineko : Oui, j’ai déjà fait une routine en plaçant la font en dessous de y=212 et utilisé des LMMM pour afficher le texte. C’est souvent comme ça qu’on procède depuis des millénaires :lol
Après ça dépend des besoins en Ram ou VRAM. :gne

Dans le Fusion-c 1.2 j’ai un exemple d’utilisation d’une font 4x4 En Ram qui s’affiche en screen2 avec des Pset.
La routine de Pset MSX1 est très très rapide, elle m’a été donnée par un généreux contributeur dont le nom m’echappe à’cet instant ... :oups Edité par ericb59 Le 24/12/2020 à 07h01


banniere-ericb59e
Site web    
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 24/12/2020 à 09h54
@aoineki : J'ai modifié la façon de faire.
Plutôt que de stocker les couleurs Foreground/Background sous la forme d'un INT, et de devoir manipuler les pointeurs pour les mettre dans le tableau de "Char", j'utilise désormais un tableau de char double dimension pour stocker les couleurs.
Code TEXT :
char color[4][2];



Ce qui m'oblige à faire 8 assignations plutôt que 4, mais de façon simple:
Code C :
 
MyLetter.Mtemp[0]=MyLetter.color[n][0];
MyLetter.Mtemp[1]=MyLetter.color[n][1];


Ce qui est beaucoup plus parlant pour moi :lol
De plus, à un cycle prêt j'obtient la même vitesse (32 cycles au lieu de 31)

Un poil d'optimisation de plus, et je suis arrivé à 29 Cycles. :D


Tips : Dans certains cas, la boucle While est plus rapide que la boucle For Edité par ericb59 Le 24/12/2020 à 10h18


banniere-ericb59e
Site web    
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2904

Le 24/12/2020 à 10h52
J'ai aussi testé les 2 et j'ai aussi gardé la copie octet par octet (cf. GitHub).
Par contre, c'est censé n'avoir aucun impact sur les perfs des prints.
A moins que tu set les couleurs avant chaque print ?


On est toujours ignorant avant de savoir.
Github    
igal Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++

Inscrit le : 29/07/2010 à 17h19

Messages: 5492

Le 24/12/2020 à 11h10
J'ai pas bien compris ce que vous essayez de dénouer mais faut dire que j'ai rdv chez l'ophtalmo qu'en février et pour le moment, j'ai beau essuyer mes verres progressifs, ca reste trouble même les lunettes au bout du nez :(

Bon...J'ai peur d'inventer la poudre mais sait on jamais :oups

Si jamais toutes vos discutions tentent de résoudre le problème de priorité d'affichage des sprites (quel joueur cache l'autre), le plus simple est d'afficher "Toujours" les sprites avec la coordonnée "Y" la plus en bas de l'écran par dessus les autres!

Si c'est pas l'sujet, oubliez ce que vous avez lu....Ce n'est que le fruit de votre imagination :fou


Tiens... voila du boudin, voila du boudin, voila du boudin... :siffle
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 24/12/2020 à 11h26
Code C :
 
typedef struct { 
    char letter;
    char letter_data;
    char x;
    char y;
    char Mtemp[8]; 
    char color[4][2];
    unsigned int letter_addr;
    unsigned int VramAddr;
} GraphPrint; 
static GraphPrint MyLetter;
void PrintCharBitmap(void)
{
  char y,n;
  y=0;
 
  MyLetter.letter_addr=((MyLetter.letter)*8);
  MyLetter.VramAddr=(MyLetter.x)+(MyLetter.y)*256;
  while (y<8)
  {
        MyLetter.letter_data = font[MyLetter.letter_addr++];
        n=MyLetter.letter_data >> 6;
        MyLetter.Mtemp[0]=MyLetter.color[n][0];
        MyLetter.Mtemp[1]=MyLetter.color[n][1];
        n=(MyLetter.letter_data >> 4) & 0b00000011;
        MyLetter.Mtemp[2]=MyLetter.color[n][0];
        MyLetter.Mtemp[3]=MyLetter.color[n][1];
        n=(MyLetter.letter_data >> 2) & 0b00000011;
        MyLetter.Mtemp[4]=MyLetter.color[n][0];
        MyLetter.Mtemp[5]=MyLetter.color[n][1];
        n=(MyLetter.letter_data) & 0b00000011;
        MyLetter.Mtemp[6]=MyLetter.color[n][0];
        MyLetter.Mtemp[7]=MyLetter.color[n][1];
        CopyRamToVram(&MyLetter.Mtemp[0], MyLetter.VramAddr, 8);
        MyLetter.VramAddr+=256;
        y++;
 
    }
}
void PrintBitmap(char *string, char x, char y, char ForeColor, char BackColor )
{
  char i=0;
      // Set the Precalculated color Array
      MyLetter.color[0][0]=BackColor;
      MyLetter.color[0][1]=BackColor;
      MyLetter.color[1][0]=BackColor;
      MyLetter.color[1][1]=ForeColor;
      MyLetter.color[2][0]=ForeColor;
      MyLetter.color[2][1]=BackColor;
      MyLetter.color[3][0]=ForeColor;
      MyLetter.color[3][1]=ForeColor;
      MyLetter.y=y;
    // Send Each Letter to the print Bitmap function
     while (string[i])
     {
         MyLetter.letter=(string[i]-32);
         MyLetter.x=x+i*8;
 
         PrintCharBitmap();
         i++;
      }
}
 


Code C :
 
 
PrintBitmap(TEXT,0,20,180,1);
 
 
Edité par ericb59 Le 24/12/2020 à 11h29


banniere-ericb59e
Site web    
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2904

Le 24/12/2020 à 15h07
igal :
Si jamais toutes vos discutions tentent de résoudre le problème de priorité d'affichage des sprites [...]


On essaye d'optimiser les fonctions d'affichage d'un caractère dans nos lib C respective.
(En vrai on s'amuse car au final tout ça n'est pas super utile dans le fond ^^)

ericb59 :
[...]


Du coup, c'est normal que tu ais un surcout à chaque Print vu que de mon coté, je ne compte pas les temps de changement de couleur (qui sont fait en dehors de la fonction Print).

Faudrait voir le code ASM généré, mais j'ai pas l'impression que ton code de décodage des bits d'une ligne soit optimal (le fait que tu le fasses octet par octet en passant par des index).
Voici l'ASM de ma lib :

Code ASM :
;D:\Dev\Private\MSX\cmsx\src\print.c:339: u16* l = &g_PrintData.Buffer[8];
;D:\Dev\Private\MSX\cmsx\src\print.c:340: *l = ((u16*)g_PrintData.Buffer)[f >> 6];
    ld    -3 (ix), a
    rlca
    rlca
    and    a, #0x03
    ld    l, a
    ld    h, #0x00
    add    hl, hl
    ld    bc, #(_g_PrintData + 0x0015)
    add    hl, bc
    ld    c, (hl)
    inc    hl
    ld    b, (hl)
    ld    ((_g_PrintData + 0x001d)), bc
;D:\Dev\Private\MSX\cmsx\src\print.c:341: *++l = ((u16*)g_PrintData.Buffer)[(f >> 4) & 0x03];
    ld    a, -3 (ix)
    rlca
    rlca
    rlca
    rlca
    and    a, #0x0f
    and    a, #0x03
    ld    l, a
    ld    h, #0x00
    add    hl, hl
    ld    bc, #(_g_PrintData + 0x0015)
    add    hl, bc
    ld    c, (hl)
    inc    hl
    ld    b, (hl)
    ld    ((_g_PrintData + 0x001f)), bc
;D:\Dev\Private\MSX\cmsx\src\print.c:342: *++l = ((u16*)g_PrintData.Buffer)[(f >> 2) & 0x03];
    ld    c, -3 (ix)
    srl    c
    srl    c
    ld    a, c
    and    a, #0x03
    ld    l, a
    ld    h, #0x00
    add    hl, hl
    ld    bc, #(_g_PrintData + 0x0015)
    add    hl, bc
    ld    c, (hl)
    inc    hl
    ld    b, (hl)
    ld    ((_g_PrintData + 0x0021)), bc
;D:\Dev\Private\MSX\cmsx\src\print.c:346: *++l = ((u16*)g_PrintData.Buffer)[f & 0x03];
    ld    a, -3 (ix)
    and    a, #0x03
    ld    l, a
    ld    h, #0x00
    add    hl, hl
    ld    bc, #(_g_PrintData + 0x0015)
    add    hl, bc
    ld    c, (hl)
    inc    hl
    ld    b, (hl)
    ld    ((_g_PrintData + 0x0023)), bc
 



Tu gagnerais peut-être aussi à mettre MyLetter.letter_addr et MyLetter.VramAddr dans des variables locales car tu laisses une chance au compilo de les garder dans les registres.
Dans l'état actuel, il va faire toujours lire/écrire en RAM, ce qui est plus lent (et ce ne sont pas des données utiles à conserver d'un Print à l'autre).


On est toujours ignorant avant de savoir.
Github    
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 24/12/2020 à 17h01
Citation :
Tu gagnerais peut-être aussi à mettre MyLetter.letter_addr et MyLetter.VramAddr dans des variables locales car tu laisses une chance au compilo de les garder dans les registres.


J'ai essayé ça ne change rien.

Je crois que c'est bon tel quel pour moi.
J'ai scindé la fonction de création du tableau de couleurs dans une fonction SetFontColor, ca ne me fait rien gagner non plus en terme de temps, mais c'est plus souple de cette façon.

Je viens de tester en mode Screen5, en faisant les quelques motifs qui s'imposaient...
J'ai exactement le même temps en Screen 5 qu'en screen 8 : 29 cycles :hum
Comme pour le Bios, j'ai le même temps.
Ce qui me parait à peut prêt cohérent vu ma fonction CopyRamToVram, envoyer 4 octets ou 8 ca ne change pas grand chose...


banniere-ericb59e
Site web    
aoineko Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : Shoutbox

Inscrit le : 02/01/2011 à 21h17

Messages: 2904

Le 24/12/2020 à 17h30
ericb59 :
Ce qui me parait à peut prêt cohérent vu ma fonction CopyRamToVram, envoyer 4 octets ou 8 ca ne change pas grand chose...


C'est surement pas loin de deux fois plus long d'envoyer 8 que 4 octets... mais comme c'est le CPU qui nous ralenti sur ce coup là, le temps d'écriture en VRAM importe peu.


On est toujours ignorant avant de savoir.
Github    
ericb59 Membre non connecté

Conseiller Municipal

Rang

Avatar

Groupe : compte ++ Groupe : Shoutbox

Inscrit le : 17/04/2012 à 10h25

Messages: 5566

Le 24/12/2020 à 18h12

Ma routine Check d’abord la vram, text si on est sur MSX1 ou pas, attends le VDP, Après tout ça 4 ou 8 octets, c’est kif-kif pour la routine à mon avis.


banniere-ericb59e
Site web    
Répondre
Vous n'êtes pas autorisé à écrire dans cette catégorie