Membuat emulator Game Boy dengan OCaml (2022)
(linoscope.github.io)- CAMLBOY adalah emulator Game Boy yang dikembangkan dengan OCaml dan berjalan di browser
- Proyek ini dipilih agar dapat benar-benar mempelajari pengembangan proyek skala menengah-besar dan cara menggunakan fitur lanjutan di OCaml
- Berbagai karakteristik bahasa OCaml seperti struktur dasar, abstraksi, GADT, functor, dan penggantian modul saat runtime dimanfaatkan secara praktis
- Berjalan pada 60FPS di browser, serta membagikan pengalaman tentang proses peningkatan performa, analisis bottleneck, dan optimisasi
- Merangkum ekosistem OCaml, otomatisasi pengujian, serta dampak pengembangan emulator terhadap peningkatan kemampuan kerja nyata
Gambaran proyek
- Selama beberapa bulan, proyek CAMLBOY dikerjakan untuk membuat emulator Game Boy dengan OCaml
- Dapat dijalankan di halaman demo dan mencakup berbagai homebrew ROM
- Repositorinya dipublikasikan di GitHub
Motivasi belajar OCaml dan latar belakang pemilihan proyek
- Saat mempelajari bahasa baru, ada keterbatasan dalam memahami cara menulis kode skala menengah/besar dan cara memanfaatkan fitur lanjutan di dunia nyata
- Untuk mengatasi masalah ini, dirasakan perlunya pengalaman proyek yang nyata, sehingga dipilihlah pengembangan emulator Game Boy
- Alasan
- Spesifikasinya jelas sehingga cakupan implementasi sudah terdefinisi
- Cukup kompleks, tetapi masih memungkinkan diselesaikan dalam beberapa bulan
- Memiliki motivasi pribadi yang kuat
Tujuan emulator
- Menulis kode dengan menekankan keterbacaan dan kemudahan pemeliharaan
- Menggunakan js_of_ocaml untuk dikompilasi ke JavaScript dan dijalankan di browser
- Mencapai FPS yang cukup untuk dimainkan bahkan di browser mobile
- Mengimplementasikan benchmark performa untuk berbagai backend compiler
Tujuan tulisan dan isi utama
Tujuan tulisan ini adalah membagikan perjalanan membuat emulator Game Boy dengan OCaml
Yang dibahas:
- Gambaran arsitektur Game Boy
- Cara menyusun kode yang mudah diuji dan tinggi keterpakaiannya kembali
- Penerapan nyata fitur lanjutan OCaml seperti functor, GADT, dan first-class module
- Pengalaman menemukan bottleneck performa, serta optimisasi dan peningkatannya
- Pandangan umum tentang OCaml
Struktur keseluruhan dan antarmuka utama
- Perangkat keras utama seperti CPU, Timer, GPU bekerja mengikuti clock yang tersinkronisasi
- Bus bertugas mengakses dan meneruskan data ke tiap modul perangkat keras berdasarkan alamat
- Tiap modul perangkat keras mengimplementasikan antarmuka
Addressable_intf.S - Seluruh bus mengikuti antarmuka
Word_addressable_intf.S
Cara kerja main loop
- Untuk sinkronisasi perangkat keras, main loop menjalankan tahap berulang berikut
- Menjalankan 1 instruksi CPU dan mencatat jumlah siklus yang terpakai
- Menjalankan Timer, GPU sebanyak jumlah siklus yang sama
- Dengan cara ini, kondisi sinkronisasi perangkat keras nyata dapat ditiru
- Disertai penjelasan beserta contoh kode implementasinya
Abstraksi baca/tulis data 8-bit dan 16-bit
- Banyak modul mengimplementasikan antarmuka input/output data 8-bit (
Addressable_intf.S) - Ekstensi baca/tulis 16-bit diwariskan dan diperluas lewat
Word_addressable_intf.S - Lapisan abstraksi disusun dengan signature dan cara include pada module type di OCaml
Implementasi bus, register, dan CPU
- Bus: menangani routing berbasis alamat ke tiap modul perangkat keras, dengan percabangan berdasarkan memory map
- Register: menyediakan antarmuka baca/tulis untuk register 8-bit dan 16-bit
- CPU: pada awalnya sangat bergantung pada bus sehingga sulit diuji
- Dengan menerapkan functor, dependensi dapat diabstraksikan dan mock dapat diinjeksi
- Hal ini membuat penulisan unit test menjadi jauh lebih mudah
Representasi instruction set (menggunakan GADT)
- Game Boy memiliki instruksi 8/16-bit, sehingga diperlukan type safety dalam definisi instruksi
- Pendekatan variant sederhana menimbulkan masalah konflik tipe nilai balik pada pattern matching yang kompleks
- Dengan menerapkan GADT (Generalized Algebraic Data Type), tipe input dan output dapat dicocokkan dengan aman
- Saat GADT diterapkan, tipe argumen dan tipe nilai balik tiap instruksi dapat diinferensikan secara akurat
- Mampu menangani pola instruksi dan parameter yang kompleks dengan aman
Cartridge dan pemilihan modul saat runtime
- Cartridge Game Boy selain ROM sederhana juga dapat menyertakan perangkat keras tambahan (MBC, timer, dll.)
- Untuk tiap tipe, diperlukan implementasi modul terpisah dan pemilihan modul yang sesuai saat runtime
- First-class module memungkinkan pergantian modul saat runtime dan memberi ekstensibilitas
Pengujian dan pengembangan eksploratif
- Memanfaatkan test ROM dan
ppx_expect- Test ROM per fungsi: memverifikasi area spesifik seperti operasi aritmetika, dukungan MBC, dan sebagainya
- Saat gagal, diagnosis yang jelas dimungkinkan melalui output layar dan lain-lain
- Integration test memberi kepercayaan saat melakukan refactor besar atau menambahkan fitur baru
- Menerapkan pendekatan pengembangan eksploratif: implementasi dan verifikasi diulang dengan test ROM
UI browser dan optimisasi performa
- Dengan js_of_ocaml, build ke JS dapat dilakukan dengan mudah
- Library Brr memungkinkan akses aman ke Javascript DOM API dengan gaya OCaml
- Performa awal (20FPS) rendah, tetapi melalui profiler Chrome dilakukan analisis bottleneck pada GPU, timer, Bigstringaf, dll.
- Commit optimisasi dilakukan per modul, dan dengan menonaktifkan inlining yang tidak efisien pada build JS, akhirnya mencapai 60FPS (PC/mobile)
- Pada build native, performanya mencapai hingga 1000FPS
Benchmark dan perbandingan hardware
- Mengimplementasikan mode benchmark headless sehingga FPS di tiap lingkungan dapat diukur
Pengembangan emulator dan kemampuan kerja nyata
- Mirip dengan competitive programming, proses menafsirkan spesifikasi yang jelas → implementasi → verifikasi diulang terus
- Menjadi pengalaman yang benar-benar membantu dalam pengembangan dan pengujian berbasis spesifikasi
Perkembangan terbaru ekosistem dan alat OCaml
- dune memberi pengalaman build system yang sederhana
- Merlin, OCamlformat memudahkan autocomplete, navigasi kode, dan formatting
- setup-ocaml juga mudah diterapkan ke Github Actions
Catatan tentang bahasa fungsional
- Ada keraguan terhadap penjelasan bahwa bahasa fungsional berarti meminimalkan side effect
- State mutable yang tersembunyi di balik abstraksi justru digunakan secara aktif demi performa
- Penulis menyukai static type, pattern matching, module system, dan type inference
Ketidaknyamanan dan biaya ketergantungan abstraksi
- Standardisasi manajemen dependensi masih rumit dan kurang penjelasan (opam, dll.)
- Jika abstraksi ditambahkan melalui struktur module-functor, keseluruhan struktur lapisan dependensi juga perlu diubah
- Tidak seperti OOP, saat memperkenalkan abstraksi, cara menulis modul dependensi tingkat atas juga harus diubah
Materi belajar yang direkomendasikan
- Learn OCaml Workshop: untuk pemula, berjalan dengan kode dan test nyata
- Real World OCaml: mempelajari gaya OCaml nyata lewat contoh praktis
- The Ultimate Game Boy Talk: video gambaran arsitektur
- gbops, Game Boy CPU Manual, Pandocs, Imran Nazar’s blog: referensi untuk instruksi dan hardware Game Boy
Kesimpulan
- Melalui proyek CAMLBOY, fitur lanjutan OCaml seperti pengujian, abstraksi, dan kompatibilitas browser dapat dialami secara praktis
- Kelebihan dan keterbatasan yang diperoleh dari perkembangan ekosistem dan pengalaman pengembangan nyata dapat dipahami dengan jelas
- Pengembangan emulator benar-benar membantu meningkatkan kemampuan pengembang tingkat menengah ke atas
1 komentar
Komentar Hacker News
Penasaran apakah ada yang bisa dengan yakin mengatakan bahwa bahasa pemrograman tertentu lebih cocok untuk menulis emulator, virtual machine, atau interpreter bytecode. Standar "lebih baik" di sini bukan performa atau mengurangi bug implementasi, melainkan apakah bahasa itu lebih intuitif saat diimplementasikan dan dieksplorasi sendiri, membuat kita belajar lebih banyak, serta menjadikan pengalaman implementasinya sendiri terasa lebih bermakna dan menyenangkan. Misalnya Erlang punya tujuan yang jelas di ranah sistem terdistribusi, dan pengetahuan domain untuk area itu selaras dengan desain bahasanya, sehingga saat dipakai kita bisa memperoleh pemahaman mendalam tentang sistem terdistribusi maupun Erlang itu sendiri. Jadi penasaran apakah ada bahasa yang targetnya adalah "mengekspresikan cara kerja mesin dalam bentuk kode"
Saya ingin menekankan bahwa bahasa pemrograman sistem seperti C, C++, Rust, dan Zig secara pribadi merupakan pilihan yang paling "memuaskan". Dalam bahasa-bahasa ini, tipe data (misalnya
uint8) langsung memetakan ke byte di memori, dan operasi sepertimemcpypada dasarnya sama dengan operasi blit. Hampir tidak ada kerepotan seperti di bahasa semacam JavaScript, ketika tipeNumberharus dipaksa dipakai sebagai byte untuk operasi bit. Saat membuat emulator dengan JavaScript, masalah seperti ini langsung terasa. Tentu saja, selama suatu bahasa mendukung tampilan grafis dan memori yang cukup, semuanya pada akhirnya bisa dipakai dengan hasil yang mirip, dan kesenangan terbesar biasanya datang saat memilih bahasa yang paling nyaman bagi diri sendiriHaskell sangat kuat untuk transformasi data yang dibutuhkan DSL dan compiler. OCaml, Lisp, dan bahasa modern yang mendukung pattern matching serta ADT juga semuanya cocok. Modern C++ juga bisa mencoba hal serupa dengan tipe seperti variant, tetapi tidak terlalu rapi. Kalau memang berniat menjalankan game sungguhan di emulator, C atau C++ adalah pilihan standar. Rust juga sepertinya cukup memungkinkan, meski saya kurang tahu soal manipulasi memori tingkat rendah di sana
Saya berpandangan tidak ada bahasa yang secara khusus lebih baik untuk membuat emulator, virtual machine, atau interpreter bytecode. Selama ada array (akses waktu konstan ke indeks sembarang) dan operasi bit, implementasinya jadi sangat mudah. Pada level yang belum mempertimbangkan JIT, bahasa fungsional pun mendukung array dan operasi bit
Saya ingin merekomendasikan sml, khususnya dialek MLTon. Ia berbagi hampir semua alasan mengapa OCaml bagus, tetapi secara pribadi saya menilainya sebagai bentuk yang lebih matang di antara bahasa keluarga ML. Satu-satunya hal yang saya rindukan dari OCaml mungkin applicative functor, tetapi ini hanya perbedaan kecil dalam struktur modul
Untuk eksperimen yang berfokus pada kesenangan di dalam browser, Elm juga opsi yang bagus. Saya juga merekomendasikan melihat proyek serupa elmboy
Tulisan ini luar biasa, bukan hanya karena OCaml-nya, tetapi juga karena merangkum proses implementasi emulator Game Boy dengan sangat baik. Saya ingin menyampaikan terima kasih kepada penulis. Selain itu, sudah lama saya punya ide bahwa jika ada SPA di browser yang menggabungkan editor assembler, assembler/linker/loader, sehingga siapa pun bisa dengan mudah merasakan pengalaman membuat homebrew Gameboy, itu akan sangat bagus untuk pendidikan pengembangan embedded
Saya penasaran apakah ada yang mencari tutorial tentang implementasi suara di emulator Game Boy. Kebanyakan tutorial tidak menjelaskan suara, dan ketika mencoba mengimplementasikannya sendiri, sulit memahami dan membuatnya hanya berdasarkan materi yang ada
Ini bukan tutorial resmi, tetapi saya membagikan materi 2 slide yang merangkum cara saya mengimplementasikannya sendiri: materi slide Suara Game Boy memiliki 4 kanal, dan setiap kanal mengeluarkan nilai antara 0~15 di setiap tick. Emulator harus menjumlahkannya (rata-rata aritmetika), menskalakannya ke rentang 0~255, lalu mengirimkannya ke buffer suara. Agar sesuai dengan tick rate (4.19MHz) dan output suara (22kHz, dll.), kira-kira satu nilai perlu dikeluarkan setiap 190 tick. Karakteristik tiap kanal dirangkum dengan baik di materi ini. Kanal 1 dan 2 adalah gelombang kotak (pengulangan 0/15), kanal 3 adalah waveform arbitrer (membaca memori), kanal 4 adalah noise berbasis LSFR. Saya juga merekomendasikan melihat contoh kode SoundModeX.java
Materi ini juga cukup bagus
Video YouTube ini juga layak dijadikan referensi
Kesan saya: tulisan yang sangat keren dan proyek yang keren juga
Yang langsung terlihat adalah demonya berjalan terlalu cepat. Checkbox Throttle hampir tidak berpengaruh. Malah saat dimatikan rasanya lebih lambat. Saat Throttle aktif hasilnya 240fps, saat nonaktif 180fps. Ketika Throttle aktif, 1 detik terasa seperti sekitar 4 detik di emulator sebenarnya. Mungkin ini berkaitan dengan refresh rate monitor yang 240Hz
requestAnimationFrame()tanpa menghitungdeltaTimeMenurut saya ini tulisan yang benar-benar indah. Terima kasih sudah membagikan materi seperti ini. Jadi ingin mencoba sendiri membuat emulator Game Boy dengan Rust, dan saya bookmark postingan blog ini karena sangat menginspirasi
Ini contoh penggunaan functor dan GADT yang benar-benar keren. Saya ingin membandingkannya dengan emulator CHIP 8 atau NES, dan sepertinya menarik juga untuk mem-port CAMLBOY ke WASM dengan ocaml-wasm
wasm_of_ocaml), kemungkinan CAMLBOY memang sudah bisa dijalankan di WASM