OCaml ile Game Boy emülatörü yapmak (2022)
(linoscope.github.io)- OCaml’i örnek düzeyinin ötesinde orta ölçekli koda uygulamak için Game Boy emülatörü CAMLBOY geliştirildi; hedef, tarayıcıda çalışması ve akıllı telefonda oynanabilir performans sunmasıydı
- Uygulama; CPU, timer ve GPU’nun CPU döngülerine yetişmesini sağlayan catch up method, adrese göre okuma/yazma yönlendirmesini üstlenen bus ve 8 bit/16 bit erişim arayüzlerinden oluşuyor
- CPU’nun test edilebilirliğini artırmak için bus uygulaması functor ile enjekte edildi; komut argümanlarının karıştırılması ise GADT ile 8 bit ve 16 bit tipleri ayrılarak azaltıldı
- Entegrasyon testleri, test ROM’ları ve
ppx_expectbirleştirilerek regresyonları yakaladı ve keşif odaklı geliştirmeyi mümkün kıldı; tarayıcı UI’ıjs_of_ocamlveBrrile uygulandı - Chrome profiler ile GPU, timer ve
Bigstringafdarboğazları azaltıldıktan sonrajs_of_ocamlinline etme kapatılarak PC tarayıcısında 100 FPS’ye, akıllı telefonda 60 FPS’ye ulaşıldı
CAMLBOY’un hedefi ve kapsamı
- CAMLBOY, OCaml ile yazılmış ve tarayıcıda çalışan bir Game Boy emülatörüdür
- Demoda çeşitli homebrew ROM’lar yer alıyor; önerilenler arasında
Bouncing ballveRocket Man Demogösteriliyor - Modern akıllı telefon tarayıcılarında da 60 FPS çalışması hedefleniyor
- Daha sonra yapılan bir PR ile
js_of_ocamltabanlı WASM çalıştırma da mümkün hale geldi - Depo linoscope/CAMLBOY üzerinde yayımlandı
Neden OCaml ile Game Boy emülatörü yapıldı?
- OCaml’i birkaç ay öğrendikten sonra basit örnekler yazılabiliyordu; ancak orta ve daha büyük ölçekli kod organizasyonu ile gelişmiş özelliklerin pratik kullanımı konusunda sezgi eksikti
- Game Boy emülatörü, alıştırma projesi için uygun koşullara sahipti
- Spesifikasyonu net olduğu için neyin uygulanacağı konusunda fazla belirsizlik yok
- Birkaç gün ya da haftada bitmeyecek kadar karmaşık
- Birkaç ay içinde tamamlanamayacak kadar aşırı karmaşık değil
- Game Boy’a dair kişisel nostalji var
- Uygulama hedeflerinde performanstan önce okunabilirlik ve bakımı kolaylık öne alındı; tarayıcıda çalıştırma ve benchmark karşılaştırmaları da kapsama dahil edildi
- js_of_ocaml ile JavaScript’e derleyip tarayıcıda çalıştırma
- Akıllı telefon tarayıcısında oynanabilir FPS’ye ulaşma
- Benchmark uygulayıp çeşitli OCaml derleyici backend’lerini karşılaştırma
Emülatör yapısı ve ana döngü
- CAMLBOY’un ana bileşenleri CPU, timer, GPU, bus, cartridge, interrupt controller, serial port, joypad gibi parçalara ayrılır
- bus, CPU ile çeşitli donanım modülleri arasında adrese göre okuma/yazma yönlendirmesi yapar
- Örneğin
0xFFFFadresine yazma, kesmeleri etkinleştirmek veya devre dışı bırakmak üzere interrupt controller’a iletilir - bus’a bağlı donanım modülleri
Addressable_intf.Sarayüzünü uygular - bus,
Word_addressable_intf.Sarayüzünü uygular
- Örneğin
- Gerçek donanımda CPU, timer ve GPU aynı clock’u paylaşır; ancak emülatör sıralı bir çalıştırma döngüsü olduğu için ayrıca senkronizasyon gerekir
- Ana döngü, her modülün ilerleme miktarını catch up method ile eşitler
- CPU bir komut çalıştırır ve tükettiği cycle sayısını kaydeder
- timer, CPU’nun tükettiği cycle sayısı kadar ilerletilir
- GPU da aynı cycle sayısı kadar ilerletilir
Okuma/yazma arayüzleri ve bus uygulaması
- 8 bit okuma/yazmayı destekleyen modüller
Addressable_intf.Simzasını paylaşırread_byte : t -> uint16 -> uint8write_byte : t -> addr:uint16 -> data:uint8 -> unitaccepts : t -> uint16 -> bool
ram.mli,gpu.mli,joypad.mli,timer.mligibi dosyalarinclude Addressable_intf.S with type t := tbiçiminde aynı arayüzü içerir- CPU ile bus arasında 16 bit okuma/yazma da gerektiğinden
Word_addressable_intf.S,Addressable_intf.S’i içerir veread_word,write_wordekler - bus; GPU, timer, RAM gibi bağlı modülleri alanlar olarak tutar ve adrese göre okuma/yazmayı uygun modüle iletir
0xC000adresindeki okuma/yazma RAM’e yönlendirilir- Tüm bellek haritası için Pandocs Memory Map referans alınır
read_word,read_byte’ı iki kez çağırarak 16 bit okumayı uygular; gerçek donanım da 16 bit erişimi iki 8 bit erişimle işler
Register’lar ve CPU test edilebilirliğinin iyileştirilmesi
- Game Boy CPU’sunda 8 bit register’lar olan
A,B,C,D,E,F,H,Lbulunur - 8 bit register’lar birleştirilerek 16 bit register’lar
AF,BC,DE,HLolarak da kullanılır - İlk CPU uygulaması
registers,bus,pcgibi öğeleri doğrudan tutan bir yapıdaydı;run_instructioniçinde fetch, decode, execute yapılıyordu - Bu yapı test etmesi zordu
- bus; GPU, timer, RAM gibi çok sayıda modüle bağımlıydı
- Birim testlerinde CPU oluşturmak için bus ve bağlı modüllerin hepsini hazırlamak gerekiyordu
- bus ve tüm bağlı modüller uygulanmadan CPU instance’ı oluşturulamıyordu
- CPU, bus’ın somut uygulamasını soyutlamak için functor olarak yeniden uygulandı
module Make (Bus : Word_addressable_intf.S)biçiminde bus uygulaması enjekte edildi- Testlerde tek bir byte array tabanlı
Mock_busile CPU instantiate edildi - Bu değişiklik sayesinde CPU birim testlerinde gerçek bus yerine mock uygulama kullanılabilir hale geldi
Komut kümesi ve GADT kullanımı
- Game Boy komut kümesinde 8 bit argüman alan komutlar ve 16 bit argüman alan komutlar vardır
ADD8 A, 0x12, 8 bitAregister’ı ile 8 bit immediate değeri toplarADD16 AF, 0x1234, 16 bitAFregister’ı ile 16 bit immediate değeri toplar
- İlk deneme, argümanları
Immediate8,Immediate16,R,RRgibi variant’larla ifade etme şeklindeydi - Variant yaklaşımında
read_arg’ın dönüş tipini tek bir tipe sabitlemek zorduR r,uint8döndürürRR rr,uint16döndürür- Aynı match ifadesi içinde dönüş tipi değişir
- Argüman tipleri GADT kullanılarak yeniden tanımlandı
Immediate8 : uint8 -> uint8 argImmediate16 : uint16 -> uint16 argR : Registers.r -> uint8 argRR : Registers.rr -> uint16 arg
- Bu yapıda
read_arg : type a. a Instruction.arg -> agibi, dönüş tipi argümanın tipine göre değişirADD8yalnızcauint8 arg * uint8 argalırADD16yalnızcauint16 arg * uint16 argalır- 8 bit ve 16 bit komut argümanlarının karıştırılması tip düzeyinde azaltılabilir
Cartridge ve birinci sınıf modüller
- Game Boy cartridge’i yalnızca basit ROM’dan ibaret değildir; türüne göre ek donanım içerebilir
ROM_ONLYtipi cartridge, oyun verisi ve kodunu saklayan yalnızca ROM içerir- Örnek olarak Tetris kullanılır
MBC3tipi cartridge, ROM’a ek olarak bağımsız RAM ve timer içerir- Örnek olarak Pokémon Red kullanılır
- Her cartridge tipinin özellikleri farklı olduğundan her biri ayrı modül olarak uygulanır
- Çalışma zamanında cartridge tipine uygun modülü seçmek için birinci sınıf modüller kullanılır
Detect_cartridge.f, ROM byte’larını alıp(module Cartridge_intf.S)döndürecek şekilde tasarlanmıştır
test ROM ve ppx_expect tabanlı entegrasyon testleri
- test ROM, emülatörün belirli bir işlevini doğrulayan programdır
- Temel aritmetik komutların davranışını kontrol etme
MBC1cartridge tipi desteğini kontrol etme
- test ROM’lar, normal oyun ROM’larından farklı olarak başarısız olan işlevin kapsamını bildirir ve bazı temel özellikler eksik olsa bile çalışabildiği için emülatör geliştirmede yararlıdır
- test ROM’lar genellikle test sonucunu ekrana yazdırır
- mooneye test ROMs, başarısızlık durumunda register dump ve assertion failure bilgisi gösterir
- blargg test roms gibi bazı test ROM’ları serial port üzerinden ASCII sonuçları yazdırır
- Entegrasyon testlerinde ppx_expect kullanılır
M.run_test_rom_and_print_framebuffer, ROM’u çalıştırır ve son ekran durumunu ASCII karakterleriyle yazdırır- Çıktı dizesi,
[%expect{|...|}]içindeki beklenen değerle karşılaştırılır ppx_expectaçıklaması için Jane Street yazısı referans alınır
- Bu test yapısı, büyük kod değişikliklerinde regresyonları yakalar ve keşif odaklı programlama akışını mümkün kılar
- Yeni özelliği doğrulayan bir test ROM bulunur
ppx_expecttesti ayarlanır- Başarısız çıktı commit edilir
- Özellik uygulanır
- Test sonucunun
Test OKdurumuna dönüp dönmediği kontrol edilir
JavaScript derleme ve tarayıcı UI’ı
- js_of_ocaml sayesinde JavaScript’e derleme zor olmadı
- Emülatörün tarayıcıda çalışır hale gelmesi için tek bir commit yeterli oldu
- Tarayıcı UI’ını uygulamak için Brr kullanıldı
- Brr, JS nesnelerini OCaml nesneleri yerine OCaml modüllerine eşler
js_of_ocamlyerleşik tarayıcı API’si JS nesnelerini OCaml nesnelerine eşlediğinden OCaml’in nesne bilgisini gerektirir- Brr kullanımı, OCaml nesne modeline ilişkin yükü azaltır
Performans optimizasyon süreci
- İlk tarayıcı çalıştırması işlevseldi ama oynamayı zorlaştıracak kadar yavaştı
- PC tarayıcısında yaklaşık 20 FPS idi
- Gerçek Game Boy 60 FPS çalıştığından performansın yaklaşık 3 kat iyileştirilmesi gerekiyordu
- Darboğazlar Chrome profiler ile bulundu
- GPU zamanın yaklaşık %73’ünü tüketiyordu
tile_data.ml%34,oam_table.ml%18,tile_map%8 tüketiyordutimer.mlve bazıBigstringaffonksiyonları da ciddi zaman tüketiyordu
- Darboğazları gidermek FPS’yi aşamalı olarak artırdı
oam_table.mloptimizasyonu: 14 FPS → 24 FPStile_data.mloptimizasyonu: 24 FPS → 35 FPStimer.mloptimizasyonu: 35 FPS → 40 FPStile_map.mloptimizasyonu: 40 FPS → 50 FPSBigstringaf.getyerineBigstringaf.unsafe_getkullanımı: 50 FPS → 60 FPS
- Sonrasında PC tarayıcısında 60 FPS’ye ulaşıldı; ancak akıllı telefonda 20~40 FPS’de kalıyordu
- Release build’in JS çıktısı dev build’den daha yavaştı; discuss.ocaml.org’un yardımıyla
js_of_ocaml’in inline etmesinin JS performans düşüşünün nedeni olduğu anlaşıldı- İlgili tartışma discuss.ocaml.org yazısında yer alıyor
- 12 Ocak 2022 güncellemesiyle olumsuz etki ocsigen/js_of_ocaml#1220 içinde ele alındı
- Inline etme devre dışı bırakıldıktan sonra PC’de 100 FPS, akıllı telefonda 60 FPS elde edildi
- JS performans optimizasyonu native performansı da iyileştirdi; native çalıştırmada yaklaşık 1000 FPS ile çalışıyor
Benchmark ve karşılaştırma sınırlamaları
- Emülatörü UI olmadan çalıştıran headless benchmarking mode uygulandı
- Çeşitli OCaml derleyici backend’lerinde FPS ölçüldü
- Bu benchmark’ı başka Game Boy emülatörleriyle FPS karşılaştırması yapmak için kullanmak zordur
- Emülatör performansı, doğruluk ve uygulanan özellik kapsamından büyük ölçüde etkilenir
- CAMLBOY, APU’yu (Audio Processing Unit) uygulamadığı için APU destekli emülatörlerle FPS karşılaştırması yapmak anlamlı değildir
OCaml kullanım deneyimi
- OCaml ekosistemi, yaklaşık 6 yıl önceki kullanım deneyimine kıyasla çok gelişti
- dune sayesinde dosyaları dizine koyunca build sisteminin bunları işlemesine yakın bir deneyim oluştu
- Merlin ve OCamlformat ile otomatik tamamlama, kod gezintisi ve otomatik formatlamayı devreye almak genel olarak kolaydı
- setup-ocaml kullanıldığında GitHub Actions’ta build ve test ayarlanabiliyor
- CAMLBOY uygulamasında performans nedeniyle mutable state yoğun kullanıldı
- Birçok modülde
t -> ... -> unittipinde fonksiyonlar var; bu, bir mutable state değişikliği anlamına geliyor - “Fonksiyonel” olmayan bir uygulama olmasına rağmen OCaml’in avantajlarının kaybedildiği hissedilmedi
- Birçok modülde
- Tercih edilen noktalar, “fonksiyonel” olmanın kendisinden çok statik tipler, variant’lar, pattern matching, modül sistemi ve iyi tip çıkarımına daha yakındı
OCaml’de rahatsız eden noktalar
- Ekosistem iyileşmiş olsa da bazı alanlar hâlâ karmaşık veya yetersiz belgelenmiş durumda
- Bağımlılıkları yeniden üretilebilir şekilde çözme sürecinde resmî opam dokümanlarında net yönlendirme eksikti
- Gerekli komutları bulmak için setup-ocaml kaynak kodu okundu
- Bir paketi yerelde “publish” edip ardından yerelde publish edilmiş paketi kurmak zorunda kalma biçimi karmaşık geldi
- Soyutlamaya bağımlılığın sözdizimsel maliyeti yüksek
B’ninC’nin somut uygulamasına değil deC_intfarayüzüne bağımlı olması istenirseB’yi functor’a çevirmek gerekirBfunctor oluncaAartık eskisi gibiB.fooya başvuramaz;Anın daB_intfalan bir functor’a çevrilmesi gerekir- Bir modülü functor’a çevirmek, yalnızca o modülün başka modüllere bağımlı olma biçimini değil, başka modüllerin o modüle bağımlı olma biçimini de değiştirir
- Bu sorun,
Camlboy -> Bus -> Cartridgebağımlılık grafiğinde yalnızcaBus -> Cartridgekısmını ayırmaya çalışırken ortaya çıkar - OOP’de class
B’nin constructor’ı somut classCyerine interfaceC_intfalacak şekilde değiştirilse bile classB’nin kendi tipi değişmez- Ancak OOP’de dynamic dispatch maliyeti vardır
- OCaml’in OOP özelliklerine aşina olmayan çok kişi olduğundan kodun okuyucu kitlesi sınırlanabilir
Kaynaklar
- OCaml ile ilgili kaynaklar
- Learn OCaml Workshop: Jane Street içinde kullanılmış atölye materyali; boşlukları olan OCaml kodu ve testleri doldurarak öğrenme yaklaşımını kullanır
- Real World OCaml: OCaml temel sözdizimini bilen veya başka fonksiyonel dil deneyimi olan kişilere önerilen, pratik örnek odaklı bir kaynaktır
- Game Boy ile ilgili kaynaklar
- The Ultimate Game Boy Talk: Game Boy mimarisini yaklaşık 1 saatte anlatan video
- gbops: Game Boy komut kümesi tablosu
- Game Boy CPU Manual: Komut uygulamasında kullanılan CPU kılavuzu; bazı kısımları, özellikle register flag çevresi, hatalıdır
- Pandocs: GPU, timer gibi donanım modüllerinin davranışı için referans alınan wiki
- Imran Nazar’s blog: JavaScript ile Game Boy emülatörü uygulama öğreticisi; uygulama kapsamını kabaca kavramak için kullanılmıştır
Henüz yorum yok.