:: PlayerAdvance.org ::  

Précédent   :: PlayerAdvance.org :: > :: Développement Amateur :: > Autres > Projets

Projets Projets de développement amateur sur d'autres supports

Publicité

Réponse
 
Outils de la discussion Modes d'affichage
Vieux 12/06/2009, 20h12   #1
Brunni
Super Modérateur
 
Date d'inscription: 10/11/2005
Localisation: Un pays avec beaucoup de banques
Messages: 3 229
Lightbulb [Projet] PatrickBoy

PatrickBoy
Console de jeu virtuelle
ALPHA
[IMG][/IMG]
Version 0.00

Présentation :
Voilà une idée de projet complètement inutile qui m'a traversé l'esprit: faire une console de jeu virtuelle. Ca m'est venu au cours de compilation, au lieu de générer du code pour MIPS, autant se faire une machine virtuelle et y ajouter un petit hardware dédié.

Galerie d'image :

Historique :
12.06.2009: sortie initiale

Comment utiliser :
-

Autres informations :
Voici donc la PatrickBoy, croisement entre la Game Boy et les jeux inutiles Super Patrick (je lui avais même fait un site dans un autre cours: http://brunni.dev-fr.org/tmp/cat).
Derrière cette présentation burlesque se cache en fait quelque chose d'assez sérieux: un projet purement software visant la définition d'un CPU "minimal" permettant d'avoir de bonnes performances malgré une fréquence relativement faible (4 MHz étaient prévus initialement, mais jusqu'à 16 sont possibles comme c'est la fréquence de la carte mère). CPU auquel on rattache un hardware permettant de faire du graphisme, du son et autres. Il faudra alors programmer un assembleur, un émulateur et éventuellement un compilateur pour un langage de haut niveau. Evidemment toutes les specs devaient être de l'ordre du faisable, donc une réflexion sur comment on pourrait l'implémenter à été faite à chaque fois, histoire de rester réaliste. Bref c'est inutile mais c'est pédagogique, au moins ça. Peut être que mon compte rendu pourra intéresser des gens, c'est pour ça que j'ai décidé de le poster ici.

Commençons donc avec le CPU: il s'agit d'un RISC 16 bits. C'est à dire que les instructions, les registres et tout sont sur 16 bits.
Il s'agit d'une architecture de Harvard, c'est à dire que le code et les données sont séparées. Pourquoi? si l'adressage est sur 16 bits on est limité au maximum à 64 ko de mémoire: RAM, ROM, VRAM tout compris. C'est bien peu... Une solution est alors de faire de la pagination (par exemple laisser 16k pour la RAM, et préciser quels 16k on adresse). La deuxième est de compacter tout ça: comme les instructions sont sur 16 bits (2 octets) on sait que toute instruction est exécutée à une adresse paire, on peut donc éliminer le dernier bit (toujours nul). Si en plus on oblige l'exécution du code en ROM seulement, on se retrouve alors avec 128 ko exécutables.

Le CPU dispose de 30 instructions au total, en comptant les différents modes: par exemple add r0, r1 (registre-registre) n'est pas la même chose que add r0, 7 (registre-littérale). L'assembleur était quelque chose d'assez intéressant à faire, mais loin d'être aussi facile qu'on le pense, comme on le verra plus tard. Donc la syntaxe ça c'est chacun qui choisit. Par exemple une routine qui remplit la palette de nuances de rouge:

Code:
    #include "patlib.h"
$routine
    mov r0, PALETTE      ; destination
    mov r1, 0            ; couleur
:boucle
    st r1, [r0+]         ; stockage dans la palette et incrémentation du pointeur
    add r1, 1            ; prochaine couleur (+1)
    bne r1, 32, :boucle  ; on va jusqu'à 32 (bne = branch if not equal)
    return
A noter que les labels sont toujours précédés d'un signe (: pour un label local, $ pour un label global). Cela permet d'alléger pas mal le langage, d'éviter les ambiguïtés (si je lis "boucle", s'agit-il d'un registre ou d'un label?). On notera qu'il n'y a toutefois pas de distinction au niveau des nombres, comme beaucoup d'assembleurs le font (#nombre). Il peut dès lors avoir des ambiguïtés: si je fais mov r0, PALETTE, s'agit-il d'un registre PALETTE ou d'une constante PALETTE? On traitera ces problèmes plus tard.

Dans le code ci-dessus si vous regardez le document vous remarquez qu'il n'existe pas d'instruction pour effectuer directement bne registre, constante 6 bits, label. Le plus proche est b[condition] registre, registre, offset. L'assembleur va alors devoir modifier l'instruction et générer le code suivant (at étant un registre réservé à l'assembleur):
Code:
    mov at, 32
    bne r1, at, -4
Pourquoi -4? Parce qu'avec l'instruction supplémentaire il faudra revenir 4 instructions en arrière (en incluant le bne lui même) pour se replacer sur l'instruction désignée par le label :boucle (st). Prenons maintenant un autre exemple:
Code:
    b :label      ; saut inconditionel 128 instructions plus loin
    space 256     ; 256 instructions
:label
On remarque alors qu'on n'a pas d'instruction permettant de faire un saut avec un offset >= 128 ou < - 128. Par contre on a une autre instruction b registre, qui utilise la valeur d'un registre pour le saut. Alors on peut le remplacer par ceci:
Code:
    mov at, 256
    b at
Mais mov at, 256 n'est pas non plus possible (il n'y a que 16 bits de place dans une instruction, et comme il faut en réserver pour l'opcode, c'est à dire "quelle est l'instruction à exécuter?" ainsi que le registre de destination, on ne peut pas avoir une constante 16 bits complète...). Par contre on peut le décomposer en deux instructions, une permettant de mettre une constante 8 bits dans un registre, et une autre permettant de mettre une constante 8 bits dans le haut d'un registre (movhi). Ainsi on se retrouve avec:
Code:
    mov at, 0
    movhi at, 1    ; 0x0100
    b at
Sur ce modèle on pourrait alors simplifier le jeu d'instruction en ne définissant que des opérations sur des registres. En effet:
Code:
    add r0, 1
pourrait devenir:
    mov at, 1
    add r0, at
En fait ce n'est vraiment pas performant, ce genre d'opérations étant réalisé souvent. J'ai alors décidé de proposer une variante des instructions add (+), sub (-), shl (<<) et shr (>>) avec une constante 4 bits non signée (0 à 15). Pour les autres c'est moins grave...
Maintenant le problème qui se pose, c'est quand générer de "gros sauts" et quand générer de petits quand on ne sait pas encore où va se placer un label? (i.e. on ne l'a pas encore rencontré)
Code:
petit saut:
    b +35
grand saut:
    mov at, 35
    movhi at, 0
    b at
Perso j'ai utilisé une heuristique: je considère au début que les labels non définis sont à l'adresse 0 et génère le code correspondant. Ensuite une fois que tout le code a été généré et que donc tous les labels sont associé à une position dans le code, je refais une passe pour voir si on peut compacter le code (comme des labels qui n'étaient pas définis avant le sont maintenant, il est possible qu'on puisse effectuer de petits sauts là où on avait dû en faire de gros avant). Et ainsi de suite jusqu'à ce qu'il n'y ait plus de changement.

Autre truc intéressant: la résolution d'expression mathématique. On peut par exemple écrire:
Code:
    mov r1, (PALETTE + 10)      ; PALETTE étant une constante
    mov r0, 31
    st r0, [r1]
Il faut les entourer de parenthèses pour éviter de lancer le converto RPN pour de bêtes constantes (il est lent, il utilise une pile). Donc ça c'est comme je l'ai dit une conversion RPN. Il est possible d'utiliser des constantes telle que PALETTE car elles sont en fait résolues par le préprocesseur... le même qui va nous permettre d'utiliser les macros comme on va le voir plus loin.

Donc nous y voilà, l'assembleur supporte aussi les macros et autres joyeusetés qui peuvent être définies à la C.
Code:
    #define add(i, j)	((i) + (j))
    mov r0, add(1, 2)   ; r0 = 3
En fait le préprocesseur se charge d'analyser chaque "mot" qui compose le fichier entré. Par mot en fait on entend chaque élément syntaxique qu'on peut regrouper. Par exemple si on voit un chiffre, alors tous les chiffres qu'on voit ensuite, le premier compris, composent un nombre. On s'arrête dès qu'on trouve quelque chose qui n'est pas un chiffre: c'est le début du prochain mot. De même, s'il commence par une lettre alors c'est un identificateur, qui se prolonge tant qu'on trouve des lettres ou des chiffres.
Le préprocesseur analyse donc l'entrée et la convertit en "mots" (en fait même une virgule est un mot, au sens que c'est un élément syntaxique). On a en sortie ce qu'on appelle des jetons: c'est à dire un texte lu associé à un élément syntaxique. A chacun de décider ce qui forme les éléments syntaxiques de son langage. Si je prends par exemple une instruction, pour moi cela représente:
Code:
    mov r0, 10
alors on a:
<jeton type="identificateur" texte="mov">
<jeton type="espace"         texte=" ">
<jeton type="identificateur" texte="r0">
<jeton type="virgule"        texte=",">
<jeton type="espace"         texte=" ">
<jeton type="nombre"         texte="10">
Bon en réalité c'est un mensonge: je ne crée pas de jeton pour les espaces. Pareil pour les commentaires. Ils sont facultatifs. Dans certains cas ils serviront par exemple à terminer un identificateur mais sinon ils n'apportent rien. Il faut savoir que pour le moment le code de conversion en jeton est ce qui rend mon assembleur très lent, principalement à cause de l'utilisation des vector et des string C++.
On peut ensuite créer un analyseur simple qui prend ces jetons en entrée et détermine quoi faire avec. Par exemple on sait qu'en début de ligne on doit avoir un nom d'instruction. On va ensuite effectuer la procédure correspondant à l'instruction qu'on a rencontrée. J'ai fait une fonction prochain qui me permet de vérifier que le prochain jeton est bien d'un type (pour la décision) et une fonction lit retournant le prochain jeton et avançant le curseur (de sorte qu'à la prochaine lecture on aura le prochain).
Prenons mov, elle est relativement complète:
Code PHP:
void MOV() {
    
/* Bon alors la syntaxe est:
        * mov registre, registre
        * mov registre, nombre
        * mov registre, label
    */
    // Dans tous les cas on lit un premier registre
    
int regDest litRegistre();
    
    
// Si le prochain est un identificateur alors on a un registre qui vient
    
if (prochain(IDENTIFICATEUR))
        
emetMovRR(regDestlitRegistre());
    
// Sinon ptet un nombre
    
else if (prochain(NOMBRE))
        
emetMovRC(regDestlitNombre());
    
// Ou un label (:lab, $lab)
    
else if (prochain(DEUXPOINTS) || prochain(DOLLAR))
        
emetMovRC(regDestlitLabel() /* plus compliqué celui là... */);
    else
        
erreur("foutage de gueule spotted");
}

int litNombre() {
    
// Conversion en entier de la chaîne lue (le nombre en question)
    
return atoi(lit()->texte);
}

int litRegistre() {
    
string texte lit()->texte;
    if (
texte == "r0")
        return 
0;
    else if (
texte == "r1")
        return 
1;
    ...

Bref je sais pas si je suis clair, mais ça paraît tout à fait plus facile de concevoir un petit langage dans ces conditions! Du moins vous pouvez comparer avec le lanagage de script de GBA Graphics si vous voulez vous faire une idée de comment ça peut être sans.
Donc voilà, ce préprocesseur, on peut alors lui ajouter deux trois trucs sympa, comme par exemple #include, qui va ouvrir un autre fichier et poursuivre la lecture sur celui-ci. Il ajoutera les jetons qu'il lit sur la même liste en sortie.
On peut aussi permettre la définition de macros à la C. Reprenons l'exemple:
Code:
    #define add(i, j)	((i) + (j))
    mov r0, add(1, 2)   ; r0 = 3
Ce qui s'est passé c'est que le préprocesseur lit simplement les éléments syntaxiques comme ils arrivent. Toutefois quelque chose peut attirer son attention: il s'agit du caractère dièse (#). Cela indique que ce qui suit le concerne lui. Il va alors lire define et savoir qu'il s'agit d'une définition de macro. La lecture du jeton '(' (PAR_GAUCHE) et des différents arguments peut se faire assez simplement, comme par exemple - avec une fonction lit(type) qui s'assure que le prochain jeton est d'un certain type et lance une erreur sinon, et consomme_si_present qui lit le prochain jeton et le jette s'il est bien d'un certain type et retourne vrai, ou ne fait rien et retourne faux sinon:
Code PHP:
lit(PAR_GAUCHE);
// Peut être qu'il y a des arguments
if (!prochain(PAR_DROITE)) {
    
liste<stringarguments;
    
// S'il ne ferme pas, alors il faut forcément un identificateur (nom de param)
    
arguments.ajoute(lit(IDENTIFICATEUR)->texte);
    
// Arguments additionnels, séparés par une virgule
    
while (consomme_si_present(VIRGULE))
        
// Prochain argument
        
arguments.ajoute(lit(IDENTIFICATEUR)->texte);
}
lit(PAR_DROITE); 
Ensuite on prend ce qu'il y a après et on l'ajoute dans une liste de jetons qui représente le texte de la macro, jusqu'à la fin de la ligne ou une virgule au niveau 0 de parenthèse si c'est un #define, ou un #endmac si c'est une #macro.
En effet, notons qu'on peut définir plusieurs macros à la suite comme ceci, impossible en C.
Code:
    #define x=r0, y=r1
    mov x, 0
    #undef x, y
J'accepte aussi un égal séparant le nom de macro de son texte, c'est plus clair ici.
Une fois une macro définie, le préprocesseur continue d'analyser le texte et de le convertir en jetons, mais une fois qu'il trouve un identificateur (un mot texte) il vérifie s'il ne fait pas partie des macros. Si c'est le cas il va vérifier s'il est suivi d'une parenthèse et commencer à rassembler les arguments, qu'il passera à la macro. Ensuite le préprocesseur va voir son entrée transférée sur le texte de la macro en question. Là aussi lorsqu'il rencontre un identificateur, il va vérifier récursivement s'il s'agit d'une macro, ou cette fois s'il s'agit d'un nom de paramètre. Il le remplacera alors par le texte lu lors de l'appel.

On a vu plus tôt que certaines instructions pouvaient paraître ambiguës. En effet si j'écris:
Code:
    mov r0, PALETTE
Qu'est-ce donc que ce PALETTE? S'il s'agit d'une constante (valant 0xF000, à tout hasard) alors c'est un nombre, sinon c'est peut être un registre, ou rien du tout (erreur). Comme l'assembleur reçoit le code déjà "préprocessé" (converti en jetons) le problème ne se posera pas: il verra mov r0 [virgule] 61440. Par contre pour le programmeur il se peut que cela pose un problème. Si on fait par exemple:
Code:
    #define r0=10
    mov r1, r0
On a une confusion! L'utilisateur pensait peut être déplacer r0 dans r1, mais en fait il y met 10. Evidemment ce cas a peu de chances d'arriver, mais on n'est pas à l'abri d'une faute d'orthographe... c'est pourquoi j'ai décidé de précéder les labels de symboles, car la plupart des instructions acceptent des labels autant que des nombres. Ainsi quelqu'un qui a défini un label pal et une constante Pal et fait ceci par inattention:
Code:
    mov r0, pal
Il va chercher bien longtemps son erreur car son registre va contenir l'adresse du label pal au lieu de la valeur qu'il a définie! Autant dire mission impossible de déboguer un tel problème, surtout si cette valeur est utilisée pour stocker quelque chose: on va aléatoirement détruire la RAM en écrivant ailleurs et provoquer des choses inattendues.

Ensuite reste à proposer de petites aides au programmeur. Par exemple vous avez vu plus haut l'instruction return. En fait il s'agit d'une pseudo instruction. Un appel réalise en fait ceci, de façon très similaire à l'ARM: il sauve la valeur courante du pc (registre spécial r15, pointeur d'instruction en train d'être exécutée, qui lors de l'exécution d'une instruction i, pointe sur i+1, c'est à dire la prochaine) dans un registre spécial appelé lr (r14) et met dans pc l'adresse de l'instruction appelée de sorte que la prochaine sur la liste sera celle-ci. Au retour il suffit alors de mettre dans pc, prochaine instruction à exécuter, la valeur de lr sauvée pour retourner juste à l'endroit où l'appel s'est fait. Si une fonction en appelle une autre, alors elle devra prendre soin de sauvegarder la valeur de lr car cet appel le modifiera. Exemple:
Code:
$main
    call :fct1
    b -1    ; boucle infinie, en effet si cette instruction est à l'adresse 0001, lors de l'exécution pc pointe sur 0002, donc le brancher à 2 - 1 = 1 lui refera exécuter éternellemetn cette même instruction

:fct1
    push lr
    call :fct2
    pop lr
    return

:fct2
    return
Là aussi les instructions push et pop utilisent un registre dédié appelé de son petit nom sp (stack pointer) qui n'est en fait autre que r13 et pointe sur le sommet de la pile. Ainsi:
Code:
    push r0
    pop r0
se transforment respectivement en:
    st r0, [-sp]
    ld r0, [sp+]
Explication: la pile pointe au début sur la fin de la RAM (0x8000). Un premier push va décrémenter cette adresse (0x7ffe) et y placer le mot à écrire. D'où la pré-décrémentation, notée [-sp], c'est à dire qu'on décrémente d'abord sp puis on y écrit le mot. Pour récupérer, c'est l'inverse: comme sp pointe déjà sur le sommet de la pile, c'est à dire le dernier élément écrit, on n'a pas besoin de pré-incrémenter, par contre on va devoir le faire après pour préparer la pile pour le prochain élément (ici 0x8000 = pile vide). D'où la post-incrémentation, notée [sp+].

Bon alors maintenant le CPU il est plus ou moins bon reste à donner une signification à ce qu'on va faire avec. Pour cela j'ai dû définir la memory map, c'est-à-dire quelle adresse correspond à quoi. A chacun de décider ce qu'on met où, selon le bon sens. Rappelez-vous qu'on a 64 ko disponibles car l'espace d'adressage est sur 16 bits (permet d'adresser simplement via un registre). Pour ma part j'ai décidé d'allouer 16 ko pour la ROM (en fait il s'agit d'une fenêtre, on pourra dire quels 16k - autrement dit quelle page de 16k on utilise afin d'accéder à toute la ROM), 16k à la RAM même s'il y en a 32, là pareil un bit dans le registre de configuration PAGECTL qu'on verra plus tard permet de choisir si c'est les 16 premiers k ou le reste. Pareil pour la VRAM, 16k là aussi. Ensuite j'ai donné 8 ko au chip son qui n'est pas implémenté pour le moment. Il reste 8k, dont 4k qui ne me sont pas utiles pour le moment et 4 autres ko, les derniers (f000 - ffff) qui seront plutôt chargés, car on va y mettre la palette (512 octets pour 256 couleurs), l'OAM (attributs des objets, aussi appelés sprites, 640 octets pour 128 sprites) et les registres de configuration, zone aussi appelée IO (derniers 2k, f800-ffff).

Concernant la RAM, on laissera la gestion 100% libre à l'utilisateur. Un truc tout de même: le BIOS de ma console copie une partie de la cartouche dans les 16 premiers ko de la RAM. Cela sert pour l'initialisation, de sorte qu'on puisse écrire ceci pour se réserver des variables pré-initialisées:
Code:
    #section ram
:var1
    space 2    ; 2 octets
:var2
    data 12    ; 2 octets, vaut 12 de base
:var3
    data8 12   ; 1 octet, vaut 12 de base
:var4
    string "Hello world"    ; liste d'octets, automatiquement complétés d'un octet nul

    #section code
$main
    mov r1, :var2
    ld r0, [r1]
    add r0, 1       ; 13
    st r0, [r1]

    mov r0, 0
    st8 r0, :var3    ; décomposé en mov at, :var3 et st8 r0, [at]
A faire attention aussi, la pile se situe à la fin des 16 premiers ko de la RAM, il faudra alors éviter de se marcher dessus (les variables, elles, commencent au début).

Pour l'interprétation de la VRAM, c'est complètement libre. J'ai décidé d'un GPU avec un certain modèle, et j'ai donné donc une certaine signification à ce qui s'y trouvait: où on met la map, les tiles, quel est leur format, etc.
Perso j'ai fait un GPU pouvant afficher 2 plans et jusqu'à 128 sprites avec des effets d'addition et de soustraction. Les 2 plans sont des maps comme d'habitude et on retrouve des registres de configuration situés dans la zone IO (adresses f800 et plus), permettant par exemple de définir la valeur de scrolling, configurer le mode graphique (bitmap 4, 8 bits ou texte), quelle map utiliser, etc. pour plus d'informations, voir le document associé.

Je vais abréger le reste car c'est assez simple si on connaît un peu le dév sur GBA par exemple. Passons maintenant à l'émulateur.
En fait cette étape a été étonnamment simple. J'ai tout d'abord créé une application Direct3D (parce que je voulais absolument la VSync et j'ai jamais compris comment l'avoir à coup sûr en OpenGL. Mais il faudrait changer ça parce que le SDK est vraiment trop lourd et Windows only).
Ensuite comme le hardware de notre console est constitué de plusieurs éléments qui travaillent indépendamment les uns les autres, il est difficile de le représenter par un logiciel. Quelque chose qui vient en tête toutefois est de décomposer l'exécution en petites unités de temps et exécuter "une partie" de ce que chacun doit faire à chaque unité de temps.
Cette unité de temps dans mon cas est le cycle d'horloge de l'oscillateur principal, celui qui est sur la carte mère. Il est cadencé à 16 MHz, et il déclenche tous les événements pouvant arriver aux éléments matériels. Il constitue donc une source idéale.
J'ai donc défini les timings: ce que fait chaque élément, et à quel fréquence. Ensuite il faut simplement à chaque cycle d'horloge effectuer les actions du matériel qui ont lieu, selon les spécifications théoriques:
* exécuter une instruction du CPU et attendre le temps qu'il l'exécute (faire autre chose pendant ce temps)
* générer un pixel de l'affichage tous les 4 cycles
* gérer les fin de ligne, fin de frame et générer les interruptions appropriées, mettre à jour les registres (DISPSTAT, n° de ligne en cours) et les waitstates en conséquence (la VRAM est plus lente d'accès si l'affichage est en cours).
* générer une sample audio tous les 381 cycles
* continuer le DMA s'il est en cours et qu'il a terminé
* décrémenter le timer et générer une interruption s'il y a lieu
* etc.
Ce modèle est louuuuuurd! En fait on va consommer énormément de CPU si on fait vraiment ça à tous les coups.
On peut toutefois optimiser en ne décrémentant par exemple pas le registre à tous les ticks d'horloge, mais par exemple calculer lorsque le programme utilisateur (le jeu en cours d'émulation quoi) le demande, le temps qui s'est écoulé depuis le dernier événement le concernant et connaître ainsi à quelle valeur il devrait être. Pareil pour l'affichage, en principe pour être précis on devrait générer pixel par pixel, mais de toute façon ce qui se passe au milieu des lignes est assez peu déterministe et un jeu ne devrait pas compter là-dessus. On dessinera alors l'affichage ligne entière par ligne. Cela déchargera énormément le CPU, et on pourra alors mettre en pause le GPU durant une période plus grande.
En effet lorsqu'un élément, par exemple le CPU, exécute quelque chose, il va ensuite se mettre en "pause", c'est-à-dire qu'il va attendre d'être réveillé lorsqu'il aura terminé. Entre temps il ne se passera rien le concernant, mais les autres composants eux vont pouvoir continuer à bouger. Pour le CPU s'il exécute une instruction qui lui a pris 2 cycles et qu'il était à 4 MHz (diviseur de fréquence par 4 comparé aux 16 MHz) alors il se mettra en pause pour 8 cycles. A chaque ligne le contrôleur vidéo se mettra en pause le temps (théorique) de dessiner sa ligne, soit 1232 cycles si ma mémoire est bonne.

Toutes les pièces s'emboîtent! Bon allez c'est l'heure de bouffer je rédigerai la suite une autre fois si ça intéresse des gens!
Je vais aussi terminer cet ému si j'ai le temps et rendre les choses plus propres. En attendant voici un lien de téléchargement. Vous pouvez compiler une petite ROM et l'exécuter avec le build.bat fourni. Le CPU et la RAM ont vraiment des clocks de merde, genre 8 kHz. C'est ultra lent, on voit les choses s'écrire à l'écran. Vous pouvez changer ça dans CPU.cpp (unsigned cpu_cycle_factor = 2048, devrait être 4 pour 4 MHz).
Pour l'instant le contrôleur (clavier), les timers et les fenêtres (dont les specs sont sujettes à changement) ne sont pas implémentées.

Screenshot:
Document: http://brunni.dev-fr.org/tmp/cat/oth...umentation.pdf
PatrickBoy tout complet: http://brunni.dev-fr.org/tmp/cat/oth.../PatrickBoy.7z

Dernière modification par Brunni ; 10/10/2009 à 10h36.
Brunni est déconnecté   Réponse avec citation

Publicité

Vieux 13/06/2009, 13h28   #2
Ass-Itch
Graphiste / Modérateur
 
Date d'inscription: 05/09/2006
Localisation: Hossegor
Messages: 2 584
Voir les codes amis Wii
Par défaut

Ce qui est cool avec Brunni c'est qu'il fait jamais des trucs pointus, que des merdouilles en 3 lignes de code. Quelqu'un peut m'expliquer le concept de "console de jeu virtuelle" ?
Ass-Itch est déconnecté   Réponse avec citation
Vieux 13/06/2009, 13h36   #3
Bobby Sixkilla
Maître Chinpoko-extra-mon
 
Date d'inscription: 10/11/2005
Localisation: Palaiseau (Rive sud)
Messages: 6 466
Voir les codes amis Nintendo DS
Par défaut

C'est comme un émulateur, sauf que la machine n'existait pas avant. Enfin, je crois... J'ai pas tout capté.
__________________
"Un pour l'argent, deux pour le spectacle et trois pour le cailloux" un putain d'énergumène
Bobby Sixkilla est déconnecté   Réponse avec citation
Vieux 13/06/2009, 14h22   #4
Brunni
Super Modérateur
 
Date d'inscription: 10/11/2005
Localisation: Un pays avec beaucoup de banques
Messages: 3 229
Par défaut

Lol j'ai pas dû être clair
C'est une console de jeu inventée. Le but c'est de voir à toutes étapes théoriques (sans vraiment faire le matériel parce que je m'y connais pas) ce qu'il y a à faire: du design de la console, ce que fait le hard, le CPU, puis les outils genre un assembleur pour pouvoir écrire du code pour la machine, un émulateur pour pouvoir tester (la console n'existe pas en vrai donc faut bien un moyen), mais aussi d'autres outils tels que GBA Graphics qui existait déjà avant, un tracker pour composer de la musique, un compilateur pour un langage plus évolué tel que le C, etc.
Bref on voit que la liste est longue, c'est d'ailleurs pour ça que je n'ai pas été jusqu'au bout.
En fait c'est une intro pour pas mal de choses dans le bas niveau, et ça peut intéresser les gens qui n'ont pas le temps de s'y mettre, au moins voir un peu comment c'est fait. Ca n'a pas d'intérêt réel en tant que tel, par contre je pense faire un vrai ému pour une vraie console depuis 0 ensuite, genre la Game Boy. Les bases sont les mêmes excepté que la console existe déjà donc y a pas la réflexion préliminaire à faire, par contre pas mal de docs à se farcir
Brunni est déconnecté   Réponse avec citation
Vieux 13/06/2009, 16h55   #5
Ayla
Membre confirmée
 
Date d'inscription: 09/08/2007
Localisation: Belfort :'(
Messages: 573
Par défaut

J'avoue que l'idée d'un système entièrement fictif et virtuel ne m'a jamais traversé l'esprit !
Vraiment très intéressant

Question : pourquoi que 30 opcodes ? Comme c'est toi qui définit ton propre CPU, pourquoi ne pas y ajouter quelques opcodes bien pratiques ? Y'en a un par exemple utilisé dans les DSP, qui effectue une addition ainsi qu'une multiplication (je ne me rappelle plus de son nom...).
Ayla est déconnecté   Réponse avec citation
Vieux 13/06/2009, 17h19   #6
Brunni
Super Modérateur
 
Date d'inscription: 10/11/2005
Localisation: Un pays avec beaucoup de banques
Messages: 3 229
Par défaut

Le but c'était d'être minimal, dans l'esprit RISC. Si j'avais vraiment voulu coder 40000 opcodes je me serais attelé à émuler un 68k par exemple
En fait si tu regardes le jeu d'instruction y a pas besoin d'aller si loin, déjà rien que faire un masque c'est compliqué:
Code:
   ; branche à bitset si r0 & 0x100 (bit 8) sans modifier r0
    mov r2, r0     ; copie de r0
    mov r1, 1
    shl r1, 8      ; r1 = 1 << 8
    and r2, r1
    bnz r2, :bitset
Besoin de 2 registres temporaires et 5 instructions...
Dans d'autres CPUs on a inclus des instructions bittest par exemple pour éviter justement ces problèmes. Mais c'est là qu'on voit la magie de ce que sait faire l'ARM, où ça donnerait:
Code:
    ands r2, r0, #0x100    ; codage constante 8 bits + décalage 5 bits (imm8m)
    bne bitset
Bon en même temps l'ARM est un jeu d'instructions 32 bits donc t'as plus de libertés, mais il est tout simplement bien pensé, mais complexe à implémenter. Le thumb (16 bits) lui est super impressionnant mais toujours aussi compact, et ça pour émuler (décoder les opérandes) c'est très lent. Un Z80 (CISC) par exemple est un bonheur en comparaison, tu lis un octet ça te dit quelle instruction c'est (donc une simple table de saut de 256 entrées pour décoder). Ensuite les opérandes éventuelles sont des octets supplémentaires.
Brunni est déconnecté   Réponse avec citation
Vieux 14/06/2009, 16h00   #7
Ass-Itch
Graphiste / Modérateur
 
Date d'inscription: 05/09/2006
Localisation: Hossegor
Messages: 2 584
Voir les codes amis Wii
Par défaut

Ok, merci pour l'explication Brunni Faut dire en tant que non-codeur absolu j'avais du mal à voir les tenants et les aboutissants d'un tel concept.
Ass-Itch est déconnecté   Réponse avec citation
Vieux 14/06/2009, 21h00   #8
Nesgba
Membre confirmé
 
Date d'inscription: 10/11/2005
Messages: 830
Par défaut

cool !

C'est sûr que ça change radicalement de l'opcode arm32, mais ça a son charme je trouve, en tout cas c'est du bon boulot que tu as fait la.
Vivement que ça soit en phase beta

je me serai bien laissé tenter par un joli voxel à l'ancienne mais la vitesse de l'emu m'a ramené à des considérations plus pragmatiques.
En attendant voici un sprite à la con qui rebondit sur les bords, asm rulez

C'est vraiment très lent.

code:
Spoiler
Fichiers attachés
Type de fichier : rar ASM_test_2.rar (148,6 Ko, 1388 affichages)
Nesgba est déconnecté   Réponse avec citation
Vieux 15/06/2009, 11h37   #9
Brunni
Super Modérateur
 
Date d'inscription: 10/11/2005
Localisation: Un pays avec beaucoup de banques
Messages: 3 229
Par défaut

Sympa, merci d'avoir testé
Nan mais je de tte vais uploader une autre version avec au moins le CPU à 4 MHz plutôt que 8 kHz, parce que là c'est pour le test on peut vraiment rien faire à cette fréquence lol
A 4 MHz par contre on ne voit rien, faudra utiliser le HALT (attente d'une interruption)...
Et juste un truc: le mode bitmap 8 bits a surtout été prévu à des fins de débogage, pas vraiment pour être utilisé étant donné qu'il n'y a pas de double buffering (pas assez de VRAM)... donc tu risques de t'embêter pour ton voxel, malgré la grosse période de VBLANK.

Dernière modification par Brunni ; 15/06/2009 à 12h21.
Brunni est déconnecté   Réponse avec citation
Vieux 23/06/2009, 23h06   #10
CrazyLapinou
Membre confirmé
 
Date d'inscription: 23/07/2007
Messages: 134
Par défaut

Salut.
C'est vraiment pas mal ! J'avoue que je comprends pas grand chose... voir rien du tout J'ai pas tout lu, j'ai sauté les parties qui sont trop techniques (j'ai du lire les 10 première lignes )
Pour le CPU, tu as utilisé un existant, ou bien tu en as "inventé" un ?
Pourquoi ne pas se baser sur un PIC, AVR, ou tout autre microcontroleur qui pourrait servir de CPU ?
Ça pourrait éventuellement permettre de faire une partie hardware adapté
Perso, je ne connais pas trop l'hardware non plus, mais je pense me lancer tranquillement. Donc si un jour tu veux adapter un hardware, ça serait sympa comme projet
A bientôt et bonne chance pour la continuation.
__________________

Mes projets:
Conker-Advance (GBA) | GoldenEye Advance (GBA)

Dernière modification par CrazyLapinou ; 23/06/2009 à 23h45.
CrazyLapinou est déconnecté   Réponse avec citation
Réponse

Liens sociaux

Tags
assembler asm tutorial

Publicité



Utilisateurs regardant la discussion actuelle : 1 (0 membre(s) et 1 invité(s))
 
Outils de la discussion
Modes d'affichage

Règles de messages
Vous ne pouvez pas créer de nouvelles discussions
Vous ne pouvez pas envoyer des réponses
Vous ne pouvez pas envoyer des pièces jointes
Vous ne pouvez pas modifier vos messages

Les balises BB sont activées : oui
Les smileys sont activés : oui
La balise [IMG] est activée : oui
Le code HTML peut être employé : non
Navigation rapide

Discussions similaires
Discussion Auteur Forum Réponses Dernier message
[PROJET] A Touch of War dem1980 [NDS] Divers 12 10/04/2006 17h05
[projet] Recherche codeur pour projet RPG thoduv [DEV] Divers 2 03/12/2005 18h46


Fuseau horaire GMT +2. Il est actuellement 08h47.


Édité par : vBulletin® version 3.7.2
Copyright ©2000 - 2023, Jelsoft Enterprises Ltd. Tous droits réservés.
Version française #16 par l'association vBulletin francophone
Design par Ass-Itch, DJP et Dr.Vince