dxn -- machine virtuelle de mort
Posté le 02/01/2022 14:53
Salut ! J'ai commencé il y a quelques jours à travailler sur une machine virtuelle et son langage ASM associé destinés au gamedev expérimental. J'apprécierais énormèment des critiques sur le format du binaire, l'addressage et la syntaxe assembleur. Je suis relativement nouveau dans ce monde, et tout peut m'aider.
Je l'ai nommé dxn. dxn combine des caractéristiques de langages que j'aime dans l'idée, mais ne fonctionnent tous pas exactement comme je le souhaiterais. Les inspirations principales de dxn sont : la programmation d'un Gameboy DMG, uxn, CHIP-8 et TIS-100.
Le fichier
concept.s de mon dépôt de développement contient un exemple de programme idéal.
sample.s et
sample.dxn représentent l'avancement actuel du projet.
dxna est l'assembleur, dxnd le désassembleur.
http://kiko.ovh/cgit/dxn/
Je veux terminer une toolchain complète pour systèmes POSIX, avec l'objectif final de porter un interpréteur dxn sur calculatrice.
Maintenant que dxna et dxnd fonctionnent, je vais pouvoir attaquer le premier interpréteur de prototypage.
Merci d'avoir lu jusqu'ici, tiens
un cookie pour ton temps
Citer : Posté le 02/01/2022 15:17 | #
il a volé mon idée...
Citer : Posté le 02/01/2022 15:46 | #
Je connais pas tout dans cette histoire, mais quelques intuitions du point de vue bas-niveau...
Sémantique de l'assembleur (ie. les concepts du langage et ce que ça calcule)
Assez clean à mon humble avis. Ça se rapproche pas mal d'un véritable assembleur. Il y a quelque points rugueux, peut-être juste parce que je suis pédant. Le plus gênant peut-être c'est que les labels montrent un certain mélange lvalue/rvalue en plus de pointeurs/valeurs. Cette ligne dit que les labels sont des pointeurs vers les données qui les suivent :
Mais cette ligne dit qu'ils sont la valeur qui suit, et qu'il y a un opérateur d'adressage :
Et ces lignes indiquent même que c'est plus que la valeur qui suit, c'est même une lvalue :
; ...
inc player_x
Généralement en assembleur le label est bien une adresse, et il y a un opérateur (par exemple noté @) pour déréférencer. Conceptuellement, ce système te donne :
mov g, player_x
mov @player_y, 0
inc @player_x
Tu peux aussi faire pareil avec un opérateur d'adressage & bien sûr. Mais c'est important que tes deux étages soient distingués explicitement (par la syntaxe, pas juste implicitement par l'opcode de l'instruction).
Après il y a aussi des détails. Si c'est noir et blanc pourquoi y a-t-il une instruction pal décrite comme "colors" ?
Le mélange RPN/opérandes est un peu bizarre, on ne sait pas trop si tu veux utiliser une pile ou si finalement tu ne veux pas. Tu as un peu le pire des deux mondes actuellement, ce que popz reflète à mon humble avis (tu es coincé entre stocker sur la pile et stocker dans tes variables).
Ta comparaison a l'air de se faire avec un bit de statut modifié par sub. Mais la comparaison signée et la non-signée sont différentes, et clairement tu n'en calcules qu'une ici. Les nombres signés étant relativement indispensables, ça veut dire que tout est du 16 bits signé... right? (tes pointeurs vont souffrir alors je préfère demander)
Tu n'as pas d'accès indexés à la pile, ce qui veut dire que tu n'as pas vraiment de variables locales. Je prédis que tu vas douiller sévère si un jour tu veux écrire une fonction récursive. :P
Syntaxe de l'assembleur
Tu ne différencies pas syntaxiquement les variables des noms de labels, ça pourrait être chiant si tu renommes et qu'il y a un conflit.
Le fait que 2F3F soit une valeur et $2F3F soit la valeur pointée, et que tu définis a comme $8000 avec bind me suggère que la seule façon de définir l'histoire des labels ci-dessus qui ne fera pas de noeuds au cerveau au programmeur c'est d'avoir les labels comme des lvalues et un opérateur d'adressage. C'est parce que sinon tu ne sais pas si trollolol est une adresse (dans le cas où c'est un label) ou si c'est une valeur dans la mémoire (dans le cas où c'est une variable définie avec bind). En particulier si tu changes une définition de label à macro et/ou inversement faut modifier tous les usages. Définir les labels comme des lvalues fait que c'est toujours une valeur.
Pourquoi est-ce qu'on ferait #bind $8000 a en se tapant ensuite la maintenance des adresses (notamment quand on supprime des variables) alors qu'on peut faire a: db 0 qui est plus court et laisse l'assembleur choisir l'adresse ? C'est plus une question de sémantique mais je vois pas pourquoi tu mets le code en lecture seule en forçant cette définition assez casse-pieds (en termes de maintenance) pour les adresses de variables.
Format du binaire
Les flags de l'instruction sont supposés indiquer les registres, mais tu n'as pas de registres ?
Si les instructions sont bien définies il n'y a en général pas besoin de flags pour identifier les lvalues/rvalues, surtout qu'il y a très peu de variations. Par exemple :
x: adresse de la case à incrémenter
movi x, y
x: adresse de la cible
y: valeur à stocker
movm x, y
x: adresse de la cible
y: adresse de la source
Bien sûr ces instructions hypothétiques movi et movm seraient toutes les deux notées mov puisque la syntaxe permet de les distinguer.
Vu ton format 16-bits où chaque unité d'adressage fait 16 bits tu n'as rien à y gagner en espace, mais c'est un peu louche de penser que tu as des flags dont plein de combinaisons sont invalides (genre mov constante constante ?).
Citer : Posté le 02/01/2022 16:22 | #
Merci beaucoup pour le retour Lephé, ça m'aide énormément.
T'as parfaitement raison pour les pointeurs, j'étais un peu confus et je ne savais pas trop que faire pour résoudre le problème. La syntaxe avec @ me plaît bien.
J'ai pas été très clair là dessus, le système est 1-bit mais pas noir et blanc. pal change la couleur RGB des états on et off de l'écran. Par exemple pal 0, 1, 0, 0 indique à la machine d'afficher tous les bits éteints en rouge.
Dans l'idée, tout est non-signé. Je ne pense pas que les nombres signés soient indispensables, mais je fais peut-être une erreur ici.
Le problème est que j'adore la RPN dans l'idée, mais ça rend le code imbitable. Je pense virer les interactions avec la pile, passer full opérandes, en laissant uniquement push et pop.
Comme dit plus haut je vais mettre la pile en retrait, les variables locales et la récursion sont intéressantes mais je ne pense pas que ce soit cohérent avec mon objectif de simplicité relative.
C'est très juste, je n'y avais pas pensé.
Je ne savais pas comment gérer ça avec mon ancienne syntaxe, mais avec ce que t'as montré plus haut ça me semble plus simple comme cela effectivement.
Je vais réécrire concept.s en prenant en compte tes remarques, merci encore !
Citer : Posté le 02/01/2022 16:54 | # | Fichier joint
J'ai fini de réécrire concept.s, je trouve en effet que les changements que tu as suggéré rendent le système plus cohérent. Surtout le truc des labels pour les données, c'est propre.
http://kiko.ovh/cgit/dxn/tree/concept.s?id=8f4f
Je pense que je vais rester sur le système de flags pour les opcodes, mais considérer les pointeurs comme des données. Les bits indiqueront donc juste quels champs déférencer. Une implémentation pourra très bien utiliser ta méthode des différents instructions, ça ne change rien en pratique. Je veux juste éviter la duplication de code.
Citer : Posté le 02/01/2022 16:56 | #
Est ce que ca t'interesse qu'on parle de ton projet à la RDP ?
Citer : Posté le 02/01/2022 16:57 | #
Oh oui je n'y avais pas pensé, merci d'avoir demandé ! Une petite mention serait appréciée
Citer : Posté le 02/01/2022 16:58 | #
Ok !
------ @RDP ------
Citer : Posté le 02/01/2022 23:45 | #
Après m'être arraché les cheveux sur un bug inexistant (shit happens), j'ai terminé la première version de la machine virtuelle. Pas de surprise, c'était le plus simple à programmer avec le désassembleur. La partie un peu tricky va se trouver sur le rendu, mais ça reste dans mon domaine de compétence. Il va falloir que je me concentre sur l'assembleur, c'est le morceau du projet le plus complexe pour moi. Je reste Confiant™
Edit à trois heures du matin
La base SDL pour dxnr est là, avec di (display init) dpx (draw pixel) et show (show...). Après ma nuit de sommeil j'ajouterai le support de drw ainsi que de jmp et variantes, avant de me remettre à travailler sur l'assembleur.
Citer : Posté le 03/01/2022 17:24 | # | Fichier joint
Tout fonctionne, je suis content/20.
dxna compile maintenant en deux passes, la première s'occupant de localiser les labels. L'assembleur devient proche de complet, il reste les commentaires en très important puis des détails.
dxnd a été amélioré pour que la sortie soit plus utile, il est notamment indiqué l'addresse de chaque instruction au début de la ligne. Ça m'a été très utile pour le debugging.
dxnr fonctionne très bien. Il n'y a pas encore d'upscaling du rendu donc c'est un peu anxiogène à regarder, mais ça s'implèmente très facilement.
dxn* dans sa globalité supporte maintenant cette partie de la spec :
^---^---^---^ ^---^---^---^
arguments opcode
$FFF0: display width (default 128)
$FFF1: display height (default 64)
$FFFD: timer, decrease by one every ms if > 0
$FFFE: instruction pointer
$FFFF: unused ; jmp #FFFF ends the program
__: db
00: noop
01: mov &, *
20: add &, *
21: sub &, *
22: mul &, *
23: div &, *
24: inc &
25: dec &
30: jmp &
31: jz *, &
32: jnz *, &
d0: di
d1: show
d4: px *, *, *
d5: xpx *, *
d6: opx *, *, *
d7: apx *, *, *
da: sp &, *, *
db: xsp &, *, *
dc: osp &, *, *
dd: asp &, *, *
ff: err
Manque plus que l'input et je pourrai porter Dumb Clicker ! On ne peut pas faire mieux.
NB : un délai artificiel est créé par la boucle loop_in_wait, sans elle dxn n'a pas le temps d'afficher une frame que l'exécution est terminée.
Voici le code source. Non commenté, donc il peut sembler bien plus compliqué qu'il ne l'est réellement.
mov #FFF0, #80
mov #FFF1, #40
di
loop_in:
xsp @mi, @mi_x, #1D
xsp @tu, @tu_x, #1D
show
mov #FFFD, #30
loop_in_wait:
jnz $FFFD, loop_in_wait
inc mi_x
dec tu_x
dec i
jnz @i, loop_in
end:
mov #FFFD, #400
end_loop_wait:
jnz $FFFD, end_loop_wait
jmp #FFFF
i:
db #41
mi_x:
db #0
tu_x:
db #7B
mi:
db #EAEA
tu:
db #AEAE
Citer : Posté le 03/01/2022 18:32 | #
Pas mal, pas mal. Quelle est la différence entre #FFFD (mov) et $FFFD (jnz) ?
J'ai essayé mais je comprends pas ce que cette instruction xsp fait pour avoir un pattern irrégulier comme ça, ni ce que le #EAEA/#AEAE vient jouer.
Citer : Posté le 03/01/2022 22:47 | # | Fichier joint
Merci !
# représente une valeur absolue, et $ est une référence. À l'exécution, $FFFD sera déférencé et la valeur pointée sera utilisée. $ est l'équivalent du @ pour les constantes.
xsp dessine un sprite 4x4 en exécutant un xor sur le canvas. Le code suivant, qui dessine le même sprite avec un offset de deux pixels, le montre bien.
mov #FFF0, #f
mov #FFF1, #f
di
main:
xsp #FFFF, #5, #5
xsp #FFFF, #6, #6
show
mov #FFFD, #1000
end_wait:
jnz $FFFD, end_wait
exit:
jmp #FFFF
Pour #EAEA et #AEAE, chaque digit hexa représentant une ligne (4 bits), AEAE est le même sprite avec les lignes paires et impaires inversées. E est 0b1111 en binaire et A est 0b1010. Si tu traces la progression des deux symboles sur une ligne, le schèma devient plus clair. À la rencontre au centre, les E et les A s'annulent grâce à l'alignement.
Citer : Posté le 03/01/2022 22:54 | #
Aaah mais oui c'est un sprite. J'étais en train de chercher une explication à base de pixels tous seuls et ça faisait trop complexe pour ça. Merci x)
Citer : Posté le 03/01/2022 23:27 | #
Dumb Clicker™ dxn™ Edition® est terminé ! Ce Dumb Clicker est le plus classe de tous par pur hasard. L'affichage binaire est juste hyper satisfaisant pour le spam compulsif.
show
loop: ;infinite loop
wait_press:
btn ;poll input events
jz $FFE4, wait_press ;wait O press
inc score
sp @score, #6, #6 ;draw score square
show
wait_rel:
btn
jnz $FFE4, wait_rel ;wait O release
jmp loop
score:
db #0
Poussé dans le dépôt du projet avec les trois autres démos. Elles sont toutes commentées
http://kiko.ovh/cgit/dxn/tree/demos
J'ai simplifié l'instruction di, son design était stupide.
La touche O est espace avec mon implémentation.
Citer : Posté le 04/01/2022 09:01 | #
Maintenant un compilateur CHIP-8 vers dxn ?
Citer : Posté le 04/01/2022 09:18 | #
de mes souvenirs pas frais de chip-8 certain trucs risque d'être non trivial…
notament le fait que la chip-8 utilise des registre de 8 bits (bonjours les if registre > 255 : registre =%256 partout)
et puis les graphisme, ça risque d'être marrant.
Après les données qui demarre avant le code et pas aux mêmes adresses ça doit se faire, tout comme le reste.
Ajouté le 04/01/2022 à 09:31 :
en fait ce serait sans doute plus simple d'implementer une vm chip-8 dans dxn
Citer : Posté le 04/01/2022 10:00 | #
notament le fait que la chip-8 utilise des registre de 8 bits (bonjours les if registre > 255 : registre =%256 partout)
et puis les graphisme, ça risque d'être marrant.
Bah le truc avec un compilo c'est que tu peux toujours définir une opération "virtuelle" ext8 ou que sais-je qui fait reg &= 255 et ensuite la générer partout. Le fait que le programme compilé soit immonde/immense c'est pas grave
Bon ça reste une blague après
Edit : Ou alors vas-y on fait un compilateur dxn -> add-in
Citer : Posté le 04/01/2022 12:03 | # | Fichier joint
Ce serait rigolo de voir un programme CHIP-8 tourner sur dxn, mais ce serait encore mieux de voir un programme dxn tourner sur CHIP-8. Vous avez deux heures
Ah update. Le sommeil c'est surcoté.
dxnr supporte de nouvelles instructions :
movp &, & : Comme mov mais déférence la deuxième valeur également. Une variable étant un pointeur, la manipulation de pointeurs se fait avec des pointeurs de pointeurs. Sans movp il n'y avait aucun moyen d'obtenir la valeur pointée par un pointeur de pointeur.
mod &, * : Opération modulo.
not & : NOT bitwise (équivalent de x=~x en C).
call *, ret : Les "fonctions" du peuple. La récursion est "limitée" à 0xFFFF appels.
push *, pop & : Permettent d'utiliser le stack utilisateur (pas le même stack que call/ret). 0xFFFF éléments avant le stack overflow.
dxnimg est un nouvel utilitaire qui permet de convertir une image grayscale en sprites dxn. Le programme parcourt l'image par blocs de 4x4, de haut à gauche jusqu'en bas à droite, et sort l'instruction db équivalent sur stdout. Programmé en C, devrait être rapide. La démo font.s a été créée en l'utilisant.
mov i, font
draw_next:
movp a, @i
sp @a, @x, #2
add x, #4
inc i
movp a, @i
jnz @a, draw_next
show
inf: ;stay open
btn
jmp inf
a:
db #0
i:
db #0
x:
db #3
font:
db #7557
db #2232
db #7143
db #3463
db #4475
db #3417
db #7517
db #2247
db #7577
db #3477
db #5757
db #7353
db #6116
db #3553
db #7137
db #1137
db #0
Je vais essayer de faire un petit jeu avec tout ça
Citer : Posté le 04/01/2022 12:09 | #
Si tu freezes la spec' à un moment je m'amuserais bien à compiler ça vers de l'assembleur.
Citer : Posté le 05/01/2022 00:17 | #
Ce serait stylé ! La spec devient de plus en plus stable, je te dirai quand ce sera posé.
Le change-mono-log yayaya !
dxna
Un bug stupide lié aux labels m'a pris des heures à dénicher, la structure contenant les labels stoquait des uint16_t dans des uint8_t. Je me déteste, c'est officiel.
dxnr
Upscale de l'affichage ! C'est enfin utilisable, et crisp as fuck.
Les instructions notb, orb, andb, xorb et dbg ont été ajoutées.
Les *b sont les opérations bitwise, c'est pratique surtout sachant que les sprites sont des entiers.
dbg prend un argument, et son comportement peut être défini par l'implémentation. dxnr écrit la valeur de l'argument et ce qu'il pointe sur stdout.
Deux nouveaux flags ont été implémentés :
FFF0 vaut 1 si le dernier inc, dec, add ou sub a underflow/overflow sa cible. Les j*f ont jarté, j*z $FFF0 ayant le même effet.
FFFC vaut 1 si le dernier xpx ou xsp a effacé au moins un pixel. Super pratique pour les collisions, même fonctionnement que CHIP-8.
J'ai sûrement oublié quelque chose.
dxnd
Solide comme un rock, pas de changement visible.
J'ai commencé à développer un petit jeu de scoring pour voir si ça s'utilise bien, je partage dès que c'est jouable
Edit 2022-1-5 2:15
Ce ne sera pas dans les specs, mais dxnr exécute sleep(0) quand noop est rencontré. Ça permet de préserver le CPU en plaçant un noop dans les boucles de délai.
Edit 2022-1-5 2:56
Macro #bind ajoutée
#bind #FFFF END
mov TIMER, #100
wait:
noop
jnz @TIMER, wait
jmp END
Citer : Posté le 05/01/2022 10:45 | #
Comment ? Je ne savais pas que sleep(0) avait un effet. Tu as une référence de de doc ou une info du même style ?