2 points par GN⁺ 2025-07-06 | 1 commentaires | Partager sur WhatsApp
  • CAMLBOY est un émulateur Game Boy développé en OCaml et exécuté dans le navigateur
  • Le projet a été choisi pour apprendre concrètement le développement de projets de taille moyenne à grande et l’usage de fonctionnalités avancées d’OCaml
  • Il exploite de manière pratique diverses caractéristiques du langage OCaml, comme la structure de base, l’abstraction, les GADT, les foncteurs et le remplacement de modules à l’exécution
  • Il fonctionne à 60 FPS dans le navigateur, et partage l’expérience de l’amélioration des performances, de l’analyse des goulots d’étranglement et de l’optimisation
  • Il fait le point sur l’écosystème OCaml, l’automatisation des tests, ainsi que sur l’impact du développement d’un émulateur sur la progression des compétences professionnelles

Vue d’ensemble du projet

  • Pendant plusieurs mois, l’auteur a mené le projet CAMLBOY et créé un émulateur Game Boy en OCaml
  • Il est possible de l’exécuter sur la page de démo, qui inclut divers ROM homebrew
  • Le dépôt est public sur GitHub

Motivation pour apprendre OCaml et raisons du choix du projet

  • Lors de l’apprentissage d’un nouveau langage, l’auteur a ressenti des limites quant à la manière d’écrire du code de taille moyenne ou grande et d’utiliser réellement des fonctionnalités avancées
  • Pour résoudre ce problème, il a jugé nécessaire d’acquérir une expérience sur un projet concret, et a donc choisi de développer un émulateur Game Boy
  • Raisons
    • Les spécifications sont claires, donc le périmètre d’implémentation est bien défini
    • Le projet est suffisamment complexe tout en restant réalisable en quelques mois
    • La motivation personnelle est forte

Objectifs de l’émulateur

  • Écrire un code mettant l’accent sur la lisibilité et la maintenabilité
  • Le compiler en JavaScript pour l’exécuter dans le navigateur avec js_of_ocaml
  • Atteindre un niveau de FPS jouable y compris sur navigateur mobile
  • Mettre en place des benchmarks de performance pour différents backends de compilation

Objectif de l’article et principaux contenus

Le but de cet article est de partager le parcours de création d’un émulateur Game Boy en OCaml
Contenus abordés :

  • Vue d’ensemble de l’architecture de la Game Boy
  • Méthodes pour structurer un code testable et hautement réutilisable
  • Mise en pratique de fonctionnalités avancées d’OCaml comme les foncteurs, GADT et modules de première classe
  • Recherche des goulots d’étranglement, optimisation et retours d’expérience sur les améliorations
  • Réflexions générales sur OCaml

Structure globale et principales interfaces

  • Les principaux composants matériels comme le CPU, le Timer et le GPU fonctionnent selon une horloge synchronisée
  • Le bus est chargé d’accéder aux données et de les transmettre à chaque module matériel selon l’adresse
  • Chaque module matériel implémente l’interface Addressable_intf.S
  • L’ensemble du bus suit l’interface Word_addressable_intf.S

Fonctionnement de la boucle principale

  • Pour synchroniser le matériel, la boucle principale exécute les étapes suivantes en cycle
    1. Exécuter une instruction CPU et enregistrer le nombre de cycles consommés
    2. Faire progresser le Timer et le GPU du même nombre de cycles
  • Cette méthode permet de reproduire l’état de synchronisation du matériel réel
  • L’article fournit des explications accompagnées d’exemples de code

Abstraction de lecture/écriture de données 8 bits et 16 bits

  • De nombreux modules implémentent une interface d’entrée/sortie de données 8 bits (Addressable_intf.S)
  • L’extension pour les lectures/écritures 16 bits hérite et ajoute des fonctionnalités via Word_addressable_intf.S
  • La couche d’abstraction est construite avec les signatures d’OCaml et le mécanisme include des types de modules

Implémentation du bus, des registres et du CPU

  • Bus : il assure le routage par adresse vers chaque module matériel, avec une logique de branchement basée sur la memory map
  • Registres : ils fournissent une interface de lecture/écriture pour les registres 8 bits et 16 bits
  • CPU : au départ, sa dépendance forte au bus rendait les tests difficiles
    • L’application de foncteurs a permis d’abstraire les dépendances et d’injecter des mocks
    • Cela a rendu l’écriture de tests unitaires beaucoup plus simple

Représentation du jeu d’instructions (usage des GADT)

  • La Game Boy possède à la fois des instructions 8 bits et 16 bits, ce qui exige une définition des instructions sûre du point de vue des types
  • Une approche par variant simple entraînait des conflits de type de valeur de retour dans les pattern matching complexes
  • L’usage des GADT (Generalized Algebraic Data Type) permet de faire correspondre en toute sécurité à la fois les types d’entrée et de sortie
  • Avec les GADT, les types des arguments et les types de retour de chaque instruction peuvent tous être inférés avec précision
  • Cela permet de gérer en toute sécurité des motifs d’instructions complexes et leurs paramètres

Cartouches et sélection de modules à l’exécution

  • Les cartouches Game Boy peuvent inclure, au-delà d’une simple ROM, du matériel additionnel (MBC, timer, etc.)
  • Il faut donc implémenter des modules distincts pour chaque type et sélectionner à l’exécution le module approprié
  • Les modules de première classe permettent ce changement de module à l’exécution ainsi que l’extensibilité

Tests et développement exploratoire

  • Utilisation de ROM de test et de ppx_expect
    • Les ROM de test par fonctionnalité permettent de valider des zones précises, comme l’arithmétique ou la prise en charge du MBC
    • En cas d’échec, un diagnostic clair est possible, par exemple via l’affichage à l’écran
  • Les tests d’intégration apportent de la confiance lors de gros refactorings et de l’ajout de nouvelles fonctionnalités
  • Une approche de développement exploratoire est appliquée : implémentation et validation sont répétées à l’aide des ROM de test

UI navigateur et optimisation des performances

  • js_of_ocaml permet de produire facilement un build JS
  • La bibliothèque Brr permet d’accéder de manière sûre aux API DOM JavaScript dans un style OCaml
  • Les performances initiales (20 FPS) étaient faibles, mais l’auteur a analysé les goulots d’étranglement dans le GPU, le timer, Bigstringaf, etc. à l’aide du profiler de Chrome
  • Des commits d’optimisation ont été appliqués à chaque module, et la désactivation d’un inlining inefficace dans le build JS a permis d’atteindre au final 60 FPS (PC/mobile)
  • En build natif, les performances montent jusqu’à 1000 FPS

Benchmarks et comparaison matérielle

  • Un mode benchmark headless a été implémenté afin de mesurer les FPS selon chaque environnement

Développement d’émulateur et compétences professionnelles

  • Comme en programmation compétitive, on répète la boucle interprétation claire des spécifications → implémentation → validation
  • C’est une expérience concrètement utile pour le développement et les tests basés sur des spécifications

Progrès récents de l’écosystème et des outils OCaml

  • dune offre l’expérience d’un système de build simple à utiliser
  • Des outils comme Merlin et OCamlformat facilitent l’autocomplétion, la navigation dans le code et le formatage
  • setup-ocaml s’intègre aussi facilement à GitHub Actions

Réflexion sur les langages fonctionnels

  • L’auteur s’interroge sur la définition des langages fonctionnels comme visant à minimiser les effets de bord
  • Des états mutables cachés derrière des abstractions sont activement utilisés pour des raisons de performance
  • L’auteur apprécie surtout les types statiques, le pattern matching, le système de modules et l’inférence de types

Inconvénients et coût des abstractions dépendantes

  • La standardisation de la gestion des dépendances reste complexe et insuffisamment documentée (opam, etc.)
  • Lorsqu’on ajoute de l’abstraction avec une structure modules-foncteurs, il faut parfois modifier jusqu’à l’architecture complète de la hiérarchie de dépendances
  • Contrairement à l’OOP, l’introduction d’abstractions impose aussi de changer la manière d’écrire les modules dépendants de plus haut niveau

Ressources d’apprentissage recommandées

Conclusion

  • Le projet CAMLBOY a permis d’expérimenter de manière concrète les fonctionnalités avancées d’OCaml, les tests, l’abstraction et la compatibilité navigateur
  • Il a aussi permis de mieux cerner les avantages et les limites issus à la fois de l’évolution de l’écosystème et de l’expérience réelle de développement
  • Le développement d’un émulateur aide concrètement les développeurs intermédiaires et avancés à progresser

1 commentaires

 
GN⁺ 2025-07-06
Avis Hacker News
  • Je me demande si quelqu’un peut affirmer avec assurance qu’un langage de programmation précis est plus adapté à l’écriture d’émulateurs, de machines virtuelles ou d’interpréteurs de bytecode. Ici, « meilleur » ne désigne ni les performances ni la réduction des erreurs d’implémentation, mais plutôt le fait que ce soit plus intuitif à implémenter et à explorer soi-même, qu’on y apprenne davantage, et que l’expérience d’implémentation elle-même soit gratifiante et amusante. Par exemple, Erlang a un objectif clair dans le domaine des systèmes distribués, et la connaissance métier de ce domaine correspond à la conception du langage, si bien qu’en l’utilisant on acquiert une compréhension approfondie des systèmes distribués et d’Erlang lui-même. Je me demande s’il existe un langage dans ce genre dont la cible serait « exprimer le fonctionnement d’une machine en code »

    • Je veux souligner que, personnellement, les langages de programmation système comme C, C++, Rust et Zig sont les choix les plus « satisfaisants ». Dans ces langages, les types de données (par ex. uint8) correspondent directement aux octets en mémoire, et des opérations comme memcpy reviennent immédiatement à des opérations de blit. On évite presque entièrement les galères qu’on a dans un langage comme JavaScript à détourner le type Number pour s’en servir comme octet destiné aux opérations binaires. Quand on écrit un émulateur en JavaScript, on se heurte tout de suite à ce genre de problème. Bien sûr, tant qu’un langage prend en charge l’affichage graphique et dispose de suffisamment de mémoire, on peut en pratique tout faire plus ou moins pareil, et au final on prendra le plus de plaisir en choisissant le langage avec lequel on est le plus à l’aise

    • Haskell excelle pour les DSL et les transformations de données nécessaires aux compilateurs. OCaml, Lisp, ainsi que les langages modernes prenant en charge le pattern matching et les ADT conviennent tous aussi. Le C++ moderne peut essayer quelque chose de similaire avec des types variant, etc., mais ce n’est pas propre. Si l’objectif est réellement de faire tourner des jeux dans l’émulateur, alors C ou C++ restent les choix standard. Rust pourrait sans doute convenir à peu près, mais je ne sais pas trop pour les manipulations mémoire bas niveau

    • Ma position est qu’il n’existe pas de langage particulièrement meilleur pour créer des émulateurs, des machines virtuelles ou des interpréteurs de bytecode. Avec des tableaux (accès en temps constant à un indice arbitraire) et des opérations sur les bits, l’implémentation devient extrêmement simple. Tant qu’on ne va pas jusqu’à considérer un JIT, les langages fonctionnels prennent eux aussi en charge les tableaux et les opérations sur les bits

    • Je recommanderais sml, et plus particulièrement le dialecte MLTon. Il partage presque toutes les raisons qui font qu’OCaml est bon, mais à mon avis c’est un langage ML plus accompli. Ce qui me manquerait dans OCaml, c’est surtout l’applicative functor, mais ce n’est pas une grande différence, seulement une structure de modules légèrement différente

    • Pour quelque chose de ludique et expérimental dans le navigateur, Elm est aussi une bonne option. Je recommande de jeter un œil à un projet similaire, elmboy

  • Cet article est une ressource vraiment formidable, non seulement pour OCaml, mais aussi pour sa présentation très solide du processus d’implémentation d’un émulateur Game Boy. J’adresse mes remerciements à l’auteur. Au passage, j’ai depuis longtemps l’idée que si l’on créait, dans le navigateur, un éditeur assembleur et une SPA regroupant assembleur, linker et loader, afin de permettre à n’importe qui d’expérimenter facilement le développement homebrew Game Boy, cela ferait un excellent outil pédagogique pour l’apprentissage du développement embarqué

    • Le projet rgbds-live ressemble à cette idée et intègre RGBDS. rgbds-live
  • Je me demande si certains cherchent des tutoriels sur l’implémentation du son dans un émulateur Game Boy. La plupart des tutoriels n’expliquent pas le son, et même en essayant de l’implémenter soi-même, il était difficile de comprendre et de réaliser quelque chose à partir de la seule documentation

    • Ce n’est pas un tutoriel officiel, mais je partage un document de 2 slides qui résume la manière dont je l’ai implémenté moi-même : slides Le son de la Game Boy comporte 4 canaux, et chaque canal produit à chaque tick une valeur entre 0 et 15. L’émulateur doit les additionner (moyenne arithmétique), les remettre à l’échelle sur une plage de 0 à 255, puis les envoyer vers le buffer audio. En fonction du tick rate (4,19 MHz) et de la sortie audio (22 kHz, etc.), il faut produire une valeur environ tous les 190 ticks. Les caractéristiques de chaque canal sont bien résumées dans cette ressource. Les canaux 1 et 2 sont des ondes carrées (répétition de 0/15), le canal 3 est une forme d’onde arbitraire (lecture mémoire), et le canal 4 est du bruit, basé sur un LSFR. Je recommande aussi de consulter le code d’exemple SoundModeX.java

    • Cette ressource est aussi plutôt bonne

    • Cette vidéo YouTube vaut aussi le détour

  • J’ai l’impression que c’est vraiment un excellent article et un projet très cool

  • Ce qui saute aux yeux, c’est que la démo tourne beaucoup trop vite. La case à cocher Throttle ne semble pas très efficace. Au contraire, quand elle est décochée, on a l’impression que c’est plus lent. Avec Throttle activé, c’est 240 fps ; désactivé, 180 fps. Quand Throttle est activé, 1 seconde y semble durer environ 4 secondes dans l’émulateur réel. Cela a probablement un lien avec le fait que l’écran a un taux de rafraîchissement de 240 Hz

    • On dirait sans doute qu’il appelle seulement requestAnimationFrame() sans calculer deltaTime
  • Je trouve cet article vraiment magnifique. Merci d’avoir partagé une telle ressource. Ça m’a donné envie d’essayer de créer moi-même un émulateur Game Boy en Rust, et comme l’article de blog a été une grande source d’inspiration, je l’ai mis en favoris

  • C’est vraiment un très bel exemple d’usage de functors et de GADT. J’aimerais le comparer à des émulateurs CHIP 8 ou NES, et il pourrait aussi être intéressant de porter CAMLBOY vers WASM avec ocaml-wasm

    • Il existe un nouveau backend WASM pour js_of_ocaml (wasm_of_ocaml), donc on peut probablement déjà faire tourner CAMLBOY en WASM