2 pontos por GN⁺ 2025-07-06 | 1 comentários | Compartilhar no WhatsApp
  • CAMLBOY é um emulador de Game Boy desenvolvido em OCaml que roda no navegador
  • Foi escolhido como projeto para aprender na prática desenvolvimento de projetos de médio a grande porte e uso de recursos avançados de OCaml
  • Utiliza de forma prática várias características da linguagem OCaml, como estrutura básica, abstração, GADT, functors e troca de módulos em tempo de execução
  • Roda a 60 FPS no navegador e compartilha a experiência de melhoria de desempenho, análise de gargalos e otimização
  • Organiza o ecossistema OCaml, a automação de testes e o impacto do desenvolvimento de emuladores na evolução das habilidades profissionais

Visão geral do projeto

  • Ao longo de alguns meses, o projeto CAMLBOY foi desenvolvido para criar um emulador de Game Boy em OCaml
  • Pode ser executado na página de demonstração e inclui várias ROMs homebrew
  • O repositório está disponível no GitHub

Motivação para aprender OCaml e contexto da escolha do projeto

  • Ao aprender uma nova linguagem, havia uma sensação de limitação em relação a como escrever código de médio/grande porte e como usar recursos avançados na prática
  • Para resolver esse problema, surgiu a necessidade de uma experiência concreta de projeto, levando à escolha de desenvolver um emulador de Game Boy
  • Motivos
    • As especificações são claras, então o escopo de implementação é bem definido
    • É complexo o suficiente, mas tem um tamanho que permite conclusão em alguns meses
    • Há uma forte motivação pessoal

Objetivos do emulador

  • Escrever código com foco em legibilidade e manutenibilidade
  • Compilar para JavaScript com js_of_ocaml para rodar no navegador
  • Alcançar FPS jogável também em navegadores móveis
  • Implementar benchmarks de desempenho para vários backends do compilador

Objetivo do texto e principais conteúdos

O objetivo deste texto é compartilhar a jornada de criar um emulador de Game Boy em OCaml
Conteúdos abordados:

  • Visão geral da arquitetura do Game Boy
  • Como estruturar código testável e altamente reutilizável
  • Uso prático de recursos avançados de OCaml, como functor, GADT e módulos de primeira classe
  • Como encontrar gargalos de desempenho, além de experiências com otimização e melhorias
  • Reflexões gerais sobre OCaml

Estrutura geral e principais interfaces

  • Os principais componentes de hardware, como CPU, Timer e GPU, operam de acordo com um clock sincronizado
  • O barramento é responsável por acessar/encaminhar dados para cada módulo de hardware conforme o endereço
  • Cada módulo de hardware implementa a interface Addressable_intf.S
  • O barramento como um todo segue a interface Word_addressable_intf.S

Como o loop principal funciona

  • Para sincronizar o hardware, o loop principal executa o seguinte ciclo
    1. Executa 1 instrução da CPU e registra quantos ciclos foram consumidos
    2. Avança o Timer e a GPU pelo mesmo número de ciclos
  • Esse método simula o estado de sincronização do hardware real
  • A implementação é explicada com exemplos de código

Abstração de leitura/escrita de dados de 8 e 16 bits

  • Vários módulos implementam a interface de entrada/saída de dados de 8 bits (Addressable_intf.S)
  • A extensão para leitura/escrita de 16 bits herda e adiciona funcionalidades por meio de Word_addressable_intf.S
  • A camada de abstração é construída usando signatures de OCaml e o mecanismo de include em tipos de módulo

Implementação do barramento, registradores e CPU

  • Barramento: responsável pelo roteamento baseado em endereço para cada módulo de hardware, com desvios segundo o mapa de memória
  • Registradores: oferecem interfaces de leitura/escrita para registradores de 8 e 16 bits
  • CPU: no início tinha forte dependência do barramento, o que dificultava os testes
    • Com a aplicação de functors, tornou-se possível abstrair dependências e injetar mocks
    • Isso tornou muito mais fácil escrever testes unitários

Representação do conjunto de instruções (uso de GADT)

  • O Game Boy possui instruções de 8 e 16 bits, então há necessidade de segurança de tipos na definição das instruções
  • A abordagem com variant simples gera problemas de conflito de tipos de retorno em pattern matching complexo
  • Ao aplicar GADT (Generalized Algebraic Data Type), torna-se possível casar com segurança tanto os tipos de entrada quanto os de saída
  • Com GADT, é possível inferir corretamente os tipos dos argumentos e dos valores de retorno de cada instrução
  • Isso permite lidar com segurança com padrões de instrução e parâmetros complexos

Cartuchos e seleção de módulos em tempo de execução

  • Cartuchos de Game Boy podem incluir hardware adicional além de uma ROM simples, como MBC e timer
  • É necessário implementar módulos separados para cada tipo e selecionar em tempo de execução o módulo adequado
  • Módulos de primeira classe viabilizam a troca de módulos em tempo de execução e a extensibilidade

Testes e desenvolvimento exploratório

  • Uso de test ROMs e ppx_expect
    • ROMs de teste por funcionalidade: validam áreas específicas como operações aritméticas e suporte a MBC
    • Em caso de falha, é possível obter diagnósticos claros, como a saída na tela
  • Testes de integração garantem confiança ao fazer grandes refatorações ou adicionar novos recursos
  • Aplicação de uma abordagem de desenvolvimento exploratório: implementação e validação repetidas com test ROMs

UI no navegador e otimização de desempenho

  • Com js_of_ocaml, é fácil gerar builds em JS
  • A biblioteca Brr permite acessar a API DOM do Javascript de forma segura no estilo de OCaml
  • O desempenho inicial (20 FPS) era baixo, mas o profiler do Chrome foi usado para analisar gargalos em GPU, timer, Bigstringaf etc.
  • Foram feitos commits de otimização em cada módulo, e ao desativar inlining ineficiente no build JS foi possível atingir 60 FPS finais (PC/móvel)
  • Em build nativo, o desempenho chega a 1000 FPS

Benchmark e comparação de hardware

  • Foi implementado um modo de benchmark headless para medir FPS em cada ambiente

Desenvolvimento de emulador e habilidades profissionais

  • De forma parecida com programação competitiva, há repetição do ciclo: interpretar especificações claras → implementar → validar
  • É uma experiência que ajuda de forma prática no desenvolvimento e teste orientados por especificações

Evolução recente do ecossistema e das ferramentas de OCaml

  • dune oferece uma experiência de sistema de build simples
  • Ferramentas como Merlin e OCamlformat facilitam autocompletar, navegação no código e formatação
  • setup-ocaml também pode ser aplicado facilmente ao GitHub Actions

Reflexões sobre linguagens funcionais

  • Há questionamento em relação à explicação de que linguagens funcionais significam minimizar efeitos colaterais
  • Estado mutável escondido sob abstrações é usado ativamente em nome do desempenho
  • O autor prefere tipagem estática, pattern matching, sistema de módulos e inferência de tipos

Incômodos e custo de dependência de abstrações

  • A padronização do gerenciamento de dependências ainda é complexa e pouco explicada (como no opam)
  • Ao adicionar abstrações com uma estrutura de módulos e functors, pode ser necessário modificar toda a estrutura da hierarquia de dependências
  • Diferentemente de OOP, ao introduzir abstrações pode ser preciso mudar até a forma de escrever módulos dependentes de nível superior

Materiais de estudo recomendados

Conclusão

  • Com o projeto CAMLBOY, foi possível vivenciar de forma prática recursos avançados de OCaml, testes, abstração e compatibilidade com navegadores
  • Também ficaram claros tanto os pontos fortes quanto as limitações obtidos com a evolução do ecossistema e a experiência real de desenvolvimento
  • Desenvolver emuladores ajuda de forma concreta a elevar o nível técnico de desenvolvedores intermediários ou mais experientes

1 comentários

 
GN⁺ 2025-07-06
Comentários do Hacker News
  • Fico curioso para saber se alguém diria com confiança que alguma linguagem de programação específica é mais adequada para escrever emuladores, máquinas virtuais e interpretadores de bytecode. Aqui, o critério de "melhor" não é desempenho nem redução de erros de implementação, mas sim o quanto é mais intuitiva para implementar e explorar por conta própria, o quanto se aprende e o quanto a própria experiência de implementar é recompensadora e divertida. Por exemplo, Erlang tem um objetivo claro no domínio de sistemas distribuídos, e o conhecimento de domínio e o design da linguagem estão alinhados com essa área, então usá-la leva a uma compreensão mais profunda tanto de sistemas distribuídos quanto do próprio Erlang. Fico me perguntando se existe alguma linguagem em que o alvo seja algo como "expressar o funcionamento de uma máquina em código"

    • Quero enfatizar que, para mim, linguagens de programação de sistemas como C, C++, Rust e Zig são as escolhas de maior "satisfação". Nessas linguagens, tipos de dados como uint8 mapeiam diretamente para bytes na memória, e operações como memcpy equivalem imediatamente a um trabalho de blit. Quase não existe o sofrimento de ficar adaptando um tipo como Number do JavaScript para usá-lo como byte em operações de bits. Ao fazer um emulador em JavaScript, você esbarra nesse tipo de problema imediatamente. Claro, desde que a linguagem ofereça exibição gráfica e memória suficiente, no fim quase qualquer uma consegue rodar algo parecido, e a maior diversão acaba vindo de escolher a linguagem com a qual você se sente mais confortável

    • Haskell tem ótimo desempenho nas transformações de dados necessárias para DSLs e compiladores. OCaml, Lisp e linguagens modernas com pattern matching e ADTs também são todas adequadas. C++ moderno também pode tentar algo parecido com tipos variant e afins, mas não fica tão limpo. Se a ideia é realmente rodar jogos no emulador, C ou C++ são as escolhas padrão. Rust talvez também dê conta razoavelmente bem, mas não sei muito sobre manipulação de memória em baixo nível

    • Sou da opinião de que não existe uma linguagem especialmente melhor para criar emuladores, máquinas virtuais e interpretadores de bytecode. Se houver arrays (acesso em tempo constante a índices arbitrários) e operações de bits, a implementação fica muito fácil. Num nível em que nem se considera JIT, até linguagens funcionais oferecem arrays e operações de bits

    • Eu recomendaria sml, mais especificamente o dialeto MLTon. Ele compartilha quase todos os motivos pelos quais OCaml é bom, mas pessoalmente eu o considero uma versão mais completa entre as linguagens da família ML. O que sinto falta do OCaml é basicamente applicative functor, mas isso é só uma diferença leve na estrutura de módulos, não algo grande

    • Para algo focado em diversão e experimentação dentro do navegador, Elm também é uma boa opção. Recomendo ver o projeto parecido elmboy

  • Este texto é excelente não só por usar OCaml, mas também por organizar muito bem o processo de implementação de um emulador de Game Boy. É um material realmente incrível. Deixo meus agradecimentos ao autor. Além disso, há muito tempo tenho a ideia de que, se alguém criasse dentro do navegador um editor de assembler e uma SPA que reunisse assembler/linker/loader, permitindo que qualquer pessoa experimentasse facilmente o desenvolvimento homebrew para Game Boy, isso seria ótimo para ensino de desenvolvimento embarcado

    • O projeto rgbds-live é parecido com essa ideia e já vem com RGBDS embutido. rgbds-live
  • Fico pensando se alguém estaria procurando um tutorial sobre implementação de som em emuladores de Game Boy. A maioria dos tutoriais não explica a parte de áudio e, mesmo tentando implementar por conta própria, foi difícil entender e construir só com o material disponível

    • Não é um tutorial oficial, mas compartilho um material de 2 slides resumindo a forma como implementei isso: slides O som do Game Boy tem 4 canais, e cada canal produz a cada tick um valor entre 0 e 15. O emulador precisa somar isso (média aritmética), escalar para a faixa de 0 a 255 e enviar para o buffer de áudio. De acordo com a taxa de ticks (4,19MHz) e a saída de áudio (22kHz etc.), deve-se emitir um valor aproximadamente a cada 190 ticks. As características de cada canal estão bem resumidas neste material. Os canais 1 e 2 são ondas quadradas (repetição de 0/15), o canal 3 é forma de onda arbitrária (leitura de memória), e o canal 4 é ruído, baseado em LSFR. Recomendo consultar o código de exemplo SoundModeX.java

    • Este material também é bem bom

    • Este vídeo no YouTube também vale a pena conferir

  • Minha impressão é de que é um texto muito bom e um projeto muito legal

  • Chamou a atenção que a demo roda rápido demais. O checkbox de Throttle quase não faz efeito. Na verdade, quando desmarca parece até ficar mais lento. Com Throttle ligado fica em 240fps, desligado em 180fps. Quando o Throttle está ligado, 1 segundo parece virar uns 4 segundos no emulador real. Provavelmente isso tem relação com o fato de o monitor ter taxa de atualização de 240Hz

    • Provavelmente estão só chamando requestAnimationFrame() e esqueceram de calcular deltaTime
  • Acho que é um texto realmente bonito. Obrigado por compartilhar esse material. Fiquei com vontade de tentar fazer eu mesmo um emulador de Game Boy em Rust, e como o post do blog foi uma grande inspiração, já deixei salvo nos favoritos

  • É um exemplo muito legal de uso de functor e GADT. Queria comparar com emuladores de CHIP 8 ou NES, e também parece interessante portar o CAMLBOY para WASM com ocaml-wasm

    • Como existe o novo backend WASM do js_of_ocaml (wasm_of_ocaml), provavelmente já dá para rodar o CAMLBOY em WASM