2 puntos por GN⁺ 2025-07-06 | 1 comentarios | Compartir por WhatsApp
  • CAMLBOY es un emulador de Game Boy desarrollado en OCaml que funciona en el navegador
  • Fue un proyecto elegido para aprender en la práctica cómo desarrollar proyectos medianos a grandes y usar funciones avanzadas de OCaml
  • Aprovecha de forma práctica varias características del lenguaje OCaml, como estructura básica, abstracción, GADT, functors y reemplazo de módulos en tiempo de ejecución
  • Funciona a 60 FPS en el navegador, y comparte la experiencia de mejora de rendimiento, análisis de cuellos de botella y optimización
  • Resume el ecosistema de OCaml, la automatización de pruebas y el impacto del desarrollo de emuladores en la mejora de habilidades profesionales

Resumen del proyecto

  • Durante varios meses se trabajó en el proyecto CAMLBOY, creando un emulador de Game Boy en OCaml
  • Se puede ejecutar desde la página de demostración e incluye varios homebrew ROM
  • El repositorio está publicado en GitHub

Motivación para aprender OCaml y por qué se eligió este proyecto

  • Al aprender un nuevo lenguaje, se sentía una limitación para entender cómo escribir código de mediana y gran escala y cómo usar realmente funciones avanzadas
  • Para resolver ese problema, se vio la necesidad de tener experiencia en un proyecto real, y por eso se eligió desarrollar un emulador de Game Boy
  • Razones
    • La especificación está clara y el alcance de implementación está definido
    • Es lo suficientemente complejo, pero de un tamaño que puede completarse en unos meses
    • Existe una fuerte motivación personal

Objetivos del emulador

  • Escribir código priorizando la legibilidad y mantenibilidad
  • Compilar a JavaScript para ejecutarlo en el navegador con js_of_ocaml
  • Alcanzar un FPS jugable también en navegadores móviles
  • Implementar benchmarks de rendimiento para distintos backends del compilador

Objetivo del artículo y contenido principal

El objetivo de este texto es compartir el recorrido de crear un emulador de Game Boy con OCaml
Temas que se tratan:

  • Panorama general de la arquitectura de Game Boy
  • Cómo estructurar código testeable y altamente reutilizable
  • Uso práctico de funciones avanzadas de OCaml como functors, GADT y módulos de primera clase
  • Cómo encontrar cuellos de botella de rendimiento, y la experiencia de optimización y mejora
  • Reflexiones generales sobre OCaml

Estructura general e interfaces principales

  • El hardware principal, como CPU, Timer y GPU, funciona siguiendo un reloj sincronizado
  • El bus se encarga de acceder y transferir datos a cada módulo de hardware según la dirección
  • Cada módulo de hardware implementa la interfaz Addressable_intf.S
  • Todo el bus sigue la interfaz Word_addressable_intf.S

Cómo funciona el bucle principal

  • Para sincronizar el hardware, en el bucle principal se ejecutan las siguientes etapas cíclicas
    1. Ejecutar 1 instrucción de CPU y registrar la cantidad de ciclos consumidos
    2. Avanzar Timer y GPU la misma cantidad de ciclos
  • Con este método se simula el estado de sincronización del hardware real
  • Se ofrece una explicación junto con ejemplos de código de implementación

Abstracción para lectura/escritura de datos de 8 y 16 bits

  • Varios módulos implementan la interfaz de entrada/salida de datos de 8 bits (Addressable_intf.S)
  • La extensión de lectura/escritura de 16 bits hereda y amplía funciones mediante Word_addressable_intf.S
  • La capa de abstracción se construye con signatures de OCaml y el uso de include en tipos de módulo

Implementación del bus, registros y CPU

  • Bus: se encarga del enrutamiento por dirección hacia cada módulo de hardware, con bifurcaciones según el mapa de memoria
  • Registros: ofrecen interfaces de lectura/escritura para registros de 8 y 16 bits
  • CPU: al inicio tenía una fuerte dependencia del bus, lo que dificultaba las pruebas
    • Aplicando functors se pudo abstraer la dependencia e inyectar mocks
    • Gracias a eso, escribir pruebas unitarias se volvió mucho más fácil

Representación del set de instrucciones (uso de GADT)

  • Game Boy tiene instrucciones de 8 y 16 bits, así que era necesario asegurar la seguridad de tipos en la definición de instrucciones
  • El enfoque con variant simple generaba problemas complejos de conflicto de tipos de retorno en pattern matching
  • Al aplicar GADT (Generalized Algebraic Data Type), fue posible hacer coincidir de forma segura tanto los tipos de entrada como los de salida
  • Con GADT, los tipos de argumentos y de retorno de cada instrucción pueden inferirse correctamente
  • Permite manejar con seguridad patrones de instrucciones complejos y sus parámetros

Cartuchos y selección de módulos en tiempo de ejecución

  • Los cartuchos de Game Boy pueden incluir hardware adicional además de una ROM simple, como MBC o temporizadores
  • Era necesario implementar módulos separados para cada tipo y seleccionar en tiempo de ejecución el módulo adecuado
  • Con módulos de primera clase se logró el cambio de módulo en runtime y la extensibilidad

Pruebas y desarrollo exploratorio

  • Uso de test ROM y ppx_expect
    • Los test ROM por función verifican áreas concretas como operaciones aritméticas o soporte MBC
    • Cuando fallan, permiten diagnósticos claros mediante salida en pantalla, entre otros métodos
  • Las pruebas de integración dan confianza al hacer grandes refactors o agregar nuevas funciones
  • Se aplicó un enfoque de desarrollo exploratorio: implementar y verificar de forma iterativa con test ROM

UI en el navegador y optimización de rendimiento

  • js_of_ocaml permite generar builds JS fácilmente
  • La biblioteca Brr permite acceder de forma segura al DOM API de JavaScript con estilo OCaml
  • El rendimiento inicial (20 FPS) era bajo, pero con el profiler de Chrome se analizaron cuellos de botella en GPU, timer, Bigstringaf y otros componentes
  • Se fueron haciendo commits de optimización por módulo, y al desactivar inlining ineficiente en el build JS se logró 60 FPS finales (PC/móvil)
  • En build nativo, el rendimiento llega hasta 1000 FPS

Benchmarks y comparación de hardware

  • Se implementó un modo de benchmark headless para medir FPS en cada entorno

Desarrollo de emuladores y habilidades profesionales

  • Igual que en la programación competitiva, se repite el ciclo de interpretar una especificación clara → implementar → verificar
  • Es una experiencia que ayuda de forma práctica al desarrollo y testing basados en especificaciones

Avances recientes en el ecosistema y herramientas de OCaml

  • dune ofrece una experiencia de sistema de build simple
  • Herramientas como Merlin y OCamlformat facilitan autocompletado, navegación de código y formateo
  • setup-ocaml también puede aplicarse fácilmente en GitHub Actions

Reflexión sobre los lenguajes funcionales

  • Se cuestiona la explicación de que un lenguaje funcional consiste en minimizar los efectos secundarios
  • El estado mutable oculto detrás de abstracciones se usa activamente por razones de rendimiento
  • El autor prefiere tipado estático, pattern matching, sistema de módulos e inferencia de tipos

Incomodidades y costo de depender de abstracciones

  • La estandarización de la gestión de dependencias sigue siendo compleja y está poco explicada (por ejemplo, opam)
  • Si se añade abstracción con una estructura de módulos y functors, también es necesario modificar toda la estructura de capas de dependencias
  • A diferencia de OOP, al introducir abstracciones también hay que cambiar la forma de escribir los módulos dependientes de nivel superior

Materiales de aprendizaje recomendados

Conclusión

  • A través del proyecto CAMLBOY, se experimentó de manera práctica con las funciones avanzadas de OCaml, pruebas, abstracción y compatibilidad con navegador, entre otros aspectos
  • También se reconocieron con claridad tanto las ventajas como las limitaciones obtenidas del avance del ecosistema y de la experiencia real de desarrollo
  • Desarrollar emuladores ayuda de forma concreta a mejorar el nivel de desarrolladores intermedios en adelante

1 comentarios

 
GN⁺ 2025-07-06
Comentarios de Hacker News
  • Me pregunto si hay alguien que pueda afirmar con seguridad que cierto lenguaje de programación es más adecuado para escribir emuladores, máquinas virtuales o intérpretes de bytecode. Aquí, el criterio de "mejor" no es el rendimiento ni reducir errores de implementación, sino qué tan intuitivo resulta al implementarlo y explorarlo uno mismo, cuánto se aprende y qué tan gratificante y divertida es la experiencia de construirlo. Por ejemplo, Erlang tiene un objetivo claro en el ámbito de los sistemas distribuidos, y el conocimiento del dominio y el diseño del lenguaje están alineados para ese campo, así que al usarlo uno obtiene una comprensión profunda tanto de los sistemas distribuidos como de Erlang en sí. Me pregunto si existe un lenguaje de ese estilo cuyo objetivo sea "expresar el funcionamiento de una máquina en código"

    • Quiero enfatizar que, personalmente, los lenguajes de programación de sistemas como C, C++, Rust y Zig son la opción más "satisfactoria". En estos lenguajes, los tipos de datos (por ejemplo, uint8) se mapean directamente a bytes en memoria, y operaciones como memcpy equivalen de inmediato a tareas de blit. Casi no hay que batallar con cosas como reutilizar el tipo Number de JavaScript como si fuera un byte para operaciones de bits. Si haces un emulador en JavaScript, te topas con ese problema enseguida. Claro, cualquier lenguaje puede servir más o menos igual si soporta gráficos y suficiente memoria, y al final uno disfruta más eligiendo el lenguaje con el que se siente más cómodo

    • Haskell muestra un gran desempeño para las transformaciones de datos necesarias en DSL y compiladores. OCaml, Lisp y los lenguajes modernos que soportan pattern matching y ADT también son adecuados. Modern C++ también puede intentar algo similar con tipos variant y demás, pero no queda tan limpio. Si de verdad quieres correr juegos en el emulador, C o C++ son la elección estándar. Rust probablemente también sirva más o menos bien, aunque no estoy seguro sobre la manipulación de memoria de bajo nivel

    • Mi postura es que no existe un lenguaje especialmente mejor para hacer emuladores, máquinas virtuales o intérpretes de bytecode. Mientras tengas arreglos (acceso en tiempo constante a índices arbitrarios) y operaciones de bits, implementarlo es muy fácil. Si ni siquiera estás pensando en JIT, los lenguajes funcionales también soportan arreglos y operaciones de bits

    • Quiero recomendar sml, y en particular el dialecto MLTon. Comparte casi todas las razones por las que OCaml es bueno, pero personalmente lo considero un producto más completo entre los lenguajes de la familia ML. Lo único que extraño de OCaml es el applicative functor, pero eso no es una gran diferencia, solo cambia un poco la estructura de módulos

    • Si la idea es divertirse y experimentar dentro del navegador, Elm también es una buena opción. Recomiendo revisar un proyecto parecido: elmboy

  • Este texto no solo trata sobre OCaml, sino que además organiza de manera muy sustanciosa el proceso de implementación de un emulador de Game Boy, así que me parece un recurso excelente. Le agradezco mucho al autor. Además, desde hace tiempo tengo la idea de que sería muy bueno para la enseñanza del desarrollo embebido crear una SPA en el navegador con editor de ensamblador, más ensamblador/linker/loader integrados, para que cualquiera pueda experimentar fácilmente con desarrollo homebrew de Game Boy

    • El proyecto rgbds-live es parecido a esta idea e incluye RGBDS integrado. rgbds-live
  • Me pregunto si alguien estará buscando tutoriales sobre cómo implementar sonido en un emulador de Game Boy. La mayoría de los tutoriales no explican el sonido, y cuando intenté implementarlo por mi cuenta, fue difícil entenderlo y construirlo solo con la documentación disponible

    • No es un tutorial oficial, pero comparto un material de 2 diapositivas que resume cómo lo implementé yo: material de diapositivas El sonido de Game Boy tiene 4 canales, y cada canal produce en cada tick un valor entre 0 y 15. El emulador debe sumarlos (promedio aritmético), escalarlos al rango 0~255 y enviarlos al búfer de sonido. De acuerdo con la tasa de ticks (4.19MHz) y la salida de audio (22kHz, etc.), se debe producir un valor aproximadamente cada 190 ticks. Las características de cada canal están bien resumidas en este material. Los canales 1 y 2 son ondas cuadradas (repetición 0/15), el canal 3 es una forma de onda arbitraria (lectura de memoria) y el canal 4 es ruido, basado en LSFR. Recomiendo revisar el código de ejemplo SoundModeX.java

    • Este material también está bastante bien

    • Este video de YouTube también puede servir de referencia

  • Me dejó la impresión de que es un artículo muy bueno y un proyecto muy cool

  • Se nota que la demo corre demasiado rápido. El checkbox de Throttle casi no hace nada. De hecho, al desactivarlo parece ir más lento. Con Throttle activado va a 240fps, y desactivado a 180fps. Cuando se activa Throttle, 1 segundo se siente como unos 4 segundos en el emulador real. Probablemente tenga que ver con que el monitor tiene una tasa de refresco de 240Hz

    • Probablemente solo esté llamando a requestAnimationFrame() y falte el cálculo de deltaTime
  • Me parece un texto realmente hermoso. Gracias por compartir un material así. Me dieron ganas de intentar hacer yo mismo un emulador de Game Boy en Rust, y como la entrada del blog me inspiró mucho, ya la guardé en marcadores

  • Es un ejemplo realmente genial de uso de functor y GADT. Me gustaría compararlo con emuladores de CHIP 8 o NES, y también sería interesante portar CAMLBOY a WASM con ocaml-wasm

    • Existe el nuevo backend WASM de js_of_ocaml (wasm_of_ocaml), así que probablemente CAMLBOY ya pueda ejecutarse en WASM