[OCaml] Maintien d'une touche avec Graphics (+compilation sous Windows ?)
Tu peux aussi juste venir tester, c'est rigolo !
Salutations, ami chameau !
TL;DR : une idée pour simuler un maintien de touche avec Graphics ? Et viens tester l'émulateur, çaytrofun et j'ai besoin de ton avis ! (cf plus bas)
Si tu as un peu de temps
Cette semaine j'ai développé un émulateur CHIP-8 en OCaml. J'ai choisi d'utiliser le module Graphics parce qu'il correspondait a priori parfaitement au cahier des charges : interface graphique très simple avec juste des carrés à dessiner, gestion basique du clavier, son crispant sortant du haut-parleur interne et module standard donc compilation simplifiée notamment sous Windows.
Seulement je me retrouve limité par ce choix puisqu'il n'y a visiblement pas moyen de détecter si l'utilisateur a relâché la touche qu'il avait enfoncée. Ainsi le programme est soumis à la configuration système de l'utilisateur.
Sous X11, on envoie un premier keypress quand la touche est enfoncée puis on attend un certain temps, et si la touche est toujours enfoncée on matraque le programme de keypress toutes les x secondes. Ces temps sont configurables (Paramètres -> Clavier) et valent par défaut respectivement 500 ms et 20 ms. Ces valeurs sont acceptables pour saisir de texte mais pas pour un jeu où il faudrait plutôt 0 ms et 20 ms.
Pour régler ce problème, j'ai mis en place un système de timeout : une pression sur une touche est gardée en mémoire pendant un certain temps, même si celle-ci est relâchée entre temps puisque ce n'est pas détectable. Ce temps est là aussi configurable par l'utilisateur. Le code ressemble à ça (version simplifiée) :
(* cette fonction est appelée à chaque nouveau cycle (pas seulement dans wait_key), et le jeu peu à tout moment demander la valeur de state.key_pressed *) let poll_key_press config state = let new_key = ref None in while Graphics.key_pressed () do let new_key := Some (Graphics.read_key ()) done; match !new_key with | Some code -> state.key_pressed <- Some code; state.pressed_since <- Unix.gettimeofday () | None -> if state.key_pressed <> None then let pressed_for = (Unix.gettimeofday () -. state.pressed_since) in if pressed_for > config.key_timeout then state.key_pressed <- None (* cette fonction est appelée quand le jeu demande une attente *) let rec wait_key config state = ignore (poll_key_press config state); (* vide la liste d'évènements clavier *) let status = Graphics.wait_next_event [Graphics.Key_pressed] in state.key_pressed <- Some code; state.pressed_since <- Unix.gettimeofday ()
Seulement ça reste du bricolage : si vous avez une meilleure solution, je suis preneur.
À défaut de meilleure solution
Mon problème principal est de savoir quelle valeur mettre par défaut pour le timeout pour les utilisateurs qui n'auraient pas pris la peine de configurer l'émulateur en fonction de leur système ?
La configuration optimale pour moi après quelques tests, c'est d'avoir le délai d'attente de X11 autour de 200 ms et la même valeur pour le timeout de l'émulateur.
Mais je me vois mal imposer le timeout de 200 ms par défaut alors que c'est incompatible avec la valeur par défaut du délai d'attente de X11 qui est, je le rappelle, 500ms : pendant 300 ms le programme croira que la touche n'est pas enfoncée, bonjour la frustration.
Et si je mets le timeout à 500 ms alors même dans le cas d'un appui qui dure une milliseconde la touche sera considérée enfoncée pendant 500 ms, provoquant là-aussi des frustrations quand on veut se déplacer un tout petit peu et que l'émulateur nous fait faire un bond énorme.
Ton avis m'intéresse
Je m'adresse donc à toi pour tester et me donner ton avis au travers d'un jeu de casse-briques. Pendant le jeu, utilise les touches 4 et 6 de ton pavé numérique pour aller respectivement à gauche ou à droite. Quand tu en as marre, tu peux appuyer sur Esc pour mettre fin au jeu.
cd /tmp wget http://dentuk.free.fr/chipeightuk.ml wget http://dentuk.free.fr/breakout.ch8 ocamlopt -o chipeightuk unix.cmxa graphics.cmxa chipeightuk.ml # frustrant non ? ./chipeightuk breakout.ch8 # -mute si tu n'aimes pas le HP interne # frustrant de manière différente, n'est-ce pas ? ./chipeightuk breakout.ch8 -kt 500 # maintenant mets le délai d'attente de X11 à 200 (Paramètres -> Clavier) ./chipeightuk breakout.ch8 # pour plus d'infos sur le programme ./chipeightuk -help less chipeightuk.ml # pour plus de jeux mkdir pack wget http://chip8.com/downloads/Chip-8%20Pack.zip unzip "Chip-8 Pack.zip" -d pack
Tant qu'on y est
J'ai un autre problème : la compilation du programme vers du natif sous Windows. En fait je n'ai même pas réussi à compiler un hello world en natif la dernière fois que j'ai essayé, j'ai abandonné après quelques tentatives infructueuses. Si quelqu'un a un document là-dessus, je suis hautement intéressé. Je réessaierai demain (tout à l'heure quoi) en donnant plus de détails sur les problèmes rencontrés.
Merci !
Réponses qui ont aidé l'auteur
5 réponse(s)
| gasche | # - le Sam 28 janvier à 11:52:32 - Répondre - Source - Cette réponse a aidé l'auteur du sujet |
|
|
Je n'ai pas trop d'avis sur la gestion du clavier. L'implémentation se lit bien; dans un style "piéton", ce qui est attendu puisque le truc à faire est plutôt simple). En geek des langages, le truc qu'on se demande immédiatement est "est-ce que ça va oufzor vite ?". Si tu t'amusais à tester des programmes demandant du CPU, tu pourrais améliorer l'efficacité de ton implémentation en décodant les opcodes dans un format facile à lire, et en interprétant ça directement, pour ne pas payer le coût du décodage de l'opcode à chaque fois que ton program counter revient sur cet opcode, comme c'est le cas aujourd'hui. Par exemple tu pourrais avoir chaque opcode sur 4 chars dans une chaîne, avec le premier correspondant demi-octets 1 et 4 souvent utilisés comme du contrôle (en mettant 4 seulement pour les instructions où il a du sens, et 0 sinon), et les demi-octets 2,3,4 ensuite. Ou alors carrément définir un type algébrique adapté, qui sépare proprement contrôle et données, en précalculant les combine_nibbles et compagnie. Le truc c'est que c'est facile à faire si la rom est non-modifiable et que le reste de la mémoire ne contient pas de code, mais sinon ça peut devenir galère. Je suppose que les programmes standard ne modifient effectivement pas la rom, mais que par contre ils s'amusent à interpréter leurs données comme des petits fous; dans ce cas là il pourrait falloir une traduction paresseuse (et invalidée par des écritures mémoires dans une zone déjà décodée), ou alors avoir un interpréteur efficace pour la rom et conserver l'interprète actuel quand le pc est en mémoire. |
| dentuk | # - le Sam 28 janvier à 16:32:22 - Répondre - Source |
|
Merci une nouvelle fois pour tes remarques ! Je m'étais pas encore intéressé à la question de l'optimisation vu que l'émulateur en lui-même n'était pas tout à fait fini, mais ça commence à venir alors puisque tu en parles j'ai regardé un peu comment s'en tirait le programme. On peut accélérer la cadence avec les options suivantes (en fait c'est la même) : -tf <frequency> Set timers frequency in Hertz (default 60.00) -tp <period> Set timers period in milliseconds (default 16.67) Sachant qu'une période des timers correspond à la lecture et l'exécution de 4 opcodes : -oc <count> Set number of opcodes executed during one period (default 4) Premier constat sur la version précédente : ce qui prend du temps, c'est les instructions «draw» (DXYN) et ce de très loin. Ce serait ridicule de vouloir optimiser la lecture des opcodes sans commencer par améliorer ça. Avec l'aide de mon ami Perl, j'ai récolté les statistiques suivantes pour le temps de lecture et d'exécution de chaque opcode sur une partie de Breakout : ici. Dans le jeu Breakout, il n'y a que deux instructions «draw» : D011 et D231. Ce sont bien entendu les instructions les plus appelées, et ces statistiques font mal : 7 ms pour la première et 1.3ms pour la seconde. Sachant que par défaut, le temps d'exécution d'un op ne devrait pas dépasser les 4 ms (16.67 ms / 4), on voit que même en configuration par défaut l'exécution est trop lente… Pour le FF0A, c'était un wait_key bloquant, donc c'était normal qu'il prenne longtemps, mais je l'ai transformé en non-bloquant : en effet pendant l'attente d'un appui sur une touche il faut que les timers soient mis à jour étant donné que c'est probablement leur principale utilité (attendre une touche pendant x secondes). Mais c'est un détail. J'ai donc jeté un œil à la fonction flip_pixel : let flip_pixel cfg x y = let x' = x mod width in let y' = y mod height in if cfg.px_mod_bounds || (x = x' && y = y') then begin let real_x = x' * cfg.px_size in let real_y = (height - (y' + 1)) * cfg.px_size in let was_on = (Graphics.point_color real_x real_y = cfg.px_on) in Graphics.set_color (if was_on then cfg.px_off else cfg.px_on); Graphics.fill_rect real_x real_y (cfg.px_size - 1) (cfg.px_size - 1); was_on end else false J'ai d'abord pensé que ça pouvait être le calcul de real_x et surtout de real_y qui ralentissaient le tout mais en après quelques tests il s'avère que c'était plutôt les appels à Graphics.point_color. Voici la nouvelle version : let flip_pixel cfg precalc state x y = let x' = x mod width in let y' = y mod height in if cfg.px_mod_bounds || (x = x' && y = y') then begin let real_x = precalc.x_coords.(x') in let real_y = precalc.y_coords.(y') in let was_on = state.screen.(y').(x') in Graphics.set_color (if was_on then cfg.px_off else cfg.px_on); Graphics.fill_rect real_x real_y (cfg.px_size - 1) (cfg.px_size - 1); state.screen.(y').(x') <- not was_on; was_on end else false Cette version affiche des résultats nettement meilleurs : ici. Ainsi maintenant D011 prend approximativement 0.6 ms pour s'exécuter et D231 0.17 ms : amélioration d'un facteur 10 ! Cependant ça reste, pour D011, 30 fois supérieur au temps d'exécution des autres opcodes donc je me demande si ça vaudrait vraiment le coup d'optimiser la gestion des opcodes sachant que le temps perdu sur les instructions DXYN sera le même, même si ça peut être intéressant sur le plan théorique. Edit : la nouvelle version est en ligne et l'ancienne a été renommée en chipeightuk.old.ml. |
| gasche | # - le Sam 28 janvier à 16:50:27 - Répondre - Source - Cette réponse a aidé l'auteur du sujet |
|
|
Bah ça dépend du genre de programmes que tu cherches à exécuter. Effectivement si ce sont des programmes interactifs où le temps d'I/O domine, la boucle interne de ton interpréteur a un impact faible sur les performances. Je pensais plutôt aux performances de programmes qui font du calcul, par exemple un programme qui calcule la somme des nombres premiers de 1 à N, qui résout le problèmes des tours de Hanoï, etc. PS: pour la compilation native sous Windows ça dépend beaucoup de ton environnement. Tu peux essayer avec le nouvel installateur Windows qui paraît-il ne marche pas trop mal (et je peux faire remonter les bugs s'il y en a). Mais quelle drôle d'idée quand même, d'utiliser Windows... |
| dentuk | # - le Sam 28 janvier à 18:51:52 - Répondre - Source |
|
Ah oui ok, personnellement c'est plus pour les programmes avec interface graphique que je compte l'utiliser, mais je comprends l'idée. Hey, merci pour le lien sur Windows ! Quelle idée en effet, mais c'est pas pour moi, c'est pour le distribuer à des gens qui n'auraient jamais vu une console de leur vie, qu'ils puissent voir ce que ça donne quand même. En fait j'avais bien installé tout ce qu'il fallait mais j'avais pas vu ces instructions donc j'essayais de compiler depuis cmd au lieu d'aller dans un shell MinGW et y avait des problèmes de path et autres… Bref maintenant tout roule, ça compile ! \o/ Bon y a quelques modifications à faire (Graphics se comporte pas de la même façon au niveau de fill_rect, de resize_window qui marche tout simplement pas alors que je l'avais utilisée pour être portable justement [faut passer par open_graph "WxH"] et d'autres fonctions) et quand je mets l'option -subsytem windows dans les FLEXLINKFLAGS pour avoir un programme GUI uniquement avast prend mon programme pour un virus, mais ça fait plaisir ! Edit : À la réflexion c'était une mauvaise idée de compiler en GUI uniquement de toute façon vu qu'il y a possibilité de sortie texte via -help et autres. Il y a visiblement aussi un problème sur le pseudo-sleep via Unix.select mais c'était prévisible, ça. D'ailleurs étant donné qu'on demande de faire une pause de moins d'une dizaine de ms, y a peu de chances que ça soit précis via sleep, non ? Une idée à ce niveau ? Edit bis : Unix.gettimeofday () sur du 32 bits c'est pas très précis… Forcément ! Edit ter : «et je peux faire remonter les bugs s'il y en a» ocamlopt avec un fichier .c dedans provoque une erreur «cc1.exe : unrecognized command-line option -mno-cygwin» (option qui a été virée des dernières versions de gcc si j'ai bien compris). |
| Poulet | # - le Dim 29 janvier à 14:07:14 - Répondre - Source |
|
|
Chez moi le jeu semble bugué (certaines briques reviennent après avoir été détruites). Sinon, je trouve ça cool et je t'encourage à continuer à nous en parler (et à en parler sur le SdZ quand ça sera fini ?). J'ai pas encore tout lu ni regardé les sources. |