• OCaml को केवल उदाहरण-स्तर से आगे बढ़ाकर मध्यम आकार के कोड पर लागू करने के लिए Game Boy emulator CAMLBOY बनाया गया, और लक्ष्य ब्राउज़र में चलना तथा स्मार्टफ़ोन पर खेलने लायक प्रदर्शन हासिल करना था
  • इम्प्लीमेंटेशन में catch up method का उपयोग किया गया, जिसमें CPU cycles के हिसाब से CPU, timer और GPU को साथ रखा जाता है; साथ ही address-based read/write routing के लिए bus और 8-bit/16-bit access interface शामिल हैं
  • CPU की testability बढ़ाने के लिए bus implementation को functor से inject किया गया, और instruction argument में भ्रम कम करने के लिए GADT के ज़रिए 8-bit और 16-bit types अलग किए गए
  • integration testing में test ROM और ppx_expect को मिलाकर regression पकड़े गए और exploratory implementation संभव हुई, जबकि browser UI को js_of_ocaml और Brr से बनाया गया
  • Chrome profiler से GPU, timer और Bigstringaf bottleneck घटाने के बाद js_of_ocaml की inlining बंद की गई, जिससे PC browser में 100 FPS और smartphone में 60 FPS हासिल हुए

CAMLBOY के लक्ष्य और दायरा

  • CAMLBOY OCaml में लिखा गया एक Game Boy emulator है, जो browser में चलता है
  • डेमो में कई homebrew ROM शामिल हैं, और Bouncing ball तथा Rocket Man Demo को विशेष रूप से सुझाया गया है
  • लक्ष्य यह था कि यह आधुनिक smartphone browser में भी 60 FPS पर चले
  • बाद में एक PR के जरिए js_of_ocaml आधारित WASM execution भी संभव हो गया
  • repository linoscope/CAMLBOY पर सार्वजनिक है

Game Boy emulator OCaml में ही क्यों बनाया

  • कई महीनों तक OCaml सीखने के बाद भी साधारण उदाहरण तो लिखे जा सकते थे, लेकिन मध्यम या उससे बड़े कोडबेस की संरचना और advanced features के व्यावहारिक उपयोग का अनुभव कम था
  • Game Boy emulator अभ्यास-प्रोजेक्ट के लिए उपयुक्त लगा
    • specification स्पष्ट है, इसलिए क्या implement करना है इस पर ज़्यादा भ्रम नहीं होता
    • इतना जटिल है कि कुछ दिनों या हफ़्तों में पूरा नहीं हो जाता
    • लेकिन इतना भी जटिल नहीं कि कुछ महीनों में पूरा ही न हो सके
    • Game Boy से व्यक्तिगत लगाव भी था
  • implementation में प्रदर्शन से पहले readability और maintainability को प्राथमिकता दी गई, और browser execution व benchmark comparison भी लक्ष्य में शामिल थे
    • js_of_ocaml से JavaScript में compile करके browser में चलाना
    • smartphone browser पर खेलने लायक FPS हासिल करना
    • benchmark बनाना और अलग-अलग OCaml compiler backends की तुलना करना

emulator की संरचना और main loop

  • CAMLBOY के मुख्य हिस्से CPU, timer, GPU, bus, cartridge, interrupt controller, serial port, joypad आदि हैं
  • bus CPU और अलग-अलग hardware modules के बीच address के आधार पर read/write route करता है
    • उदाहरण के लिए 0xFFFF address पर write interrupt controller को भेजा जाता है, जिससे interrupt enable या disable होता है
    • bus से जुड़े hardware modules Addressable_intf.S interface implement करते हैं
    • bus स्वयं Word_addressable_intf.S interface implement करता है
  • असली hardware में CPU, timer और GPU एक ही clock साझा करते हैं, लेकिन emulator sequential loop में चलता है, इसलिए अलग synchronization चाहिए
  • main loop में catch up method का उपयोग होता है
    • CPU एक instruction execute करता है और consumed cycles की संख्या दर्ज करता है
    • timer को CPU द्वारा consumed cycles के बराबर आगे बढ़ाया जाता है
    • GPU को भी उतने ही cycles आगे बढ़ाया जाता है

read/write interface और bus implementation

  • 8-bit read/write सपोर्ट करने वाले modules Addressable_intf.S signature साझा करते हैं
    • read_byte : t -> uint16 -> uint8
    • write_byte : t -> addr:uint16 -> data:uint8 -> unit
    • accepts : t -> uint16 -> bool
  • ram.mli, gpu.mli, joypad.mli, timer.mli आदि include Addressable_intf.S with type t := t के रूप में वही interface शामिल करते हैं
  • CPU और bus के बीच 16-bit read/write भी चाहिए, इसलिए Word_addressable_intf.S, Addressable_intf.S को include करके read_word और write_word जोड़ता है
  • bus में GPU, timer, RAM जैसे जुड़े modules fields के रूप में होते हैं, और address के आधार पर सही module को read/write भेजा जाता है
    • 0xC000 address का read/write RAM को route होता है
    • पूरा memory map Pandocs Memory Map में देखा जा सकता है
  • read_word को read_byte दो बार call करके implement किया गया है, और असली hardware भी 16-bit access को दो 8-bit accesses से संभालता है

registers और CPU testability में सुधार

  • Game Boy CPU में 8-bit registers A, B, C, D, E, F, H, L होते हैं
  • इन्हें जोड़कर 16-bit registers AF, BC, DE, HL की तरह भी इस्तेमाल किया जाता है
  • शुरुआती CPU implementation में registers, bus, pc आदि सीधे मौजूद थे, और run_instruction में fetch, decode, execute किया जाता था
  • यह संरचना test करना कठिन बनाती थी
    • bus, GPU, timer, RAM जैसे कई modules पर निर्भर था
    • unit test में CPU बनाने के लिए bus और उससे जुड़े सभी modules तैयार करने पड़ते थे
    • bus और उसके सभी modules implement होने से पहले CPU instance बनाना संभव नहीं था
  • CPU को functor के रूप में दोबारा implement करके bus की concrete implementation को abstract किया गया
    • module Make (Bus : Word_addressable_intf.S) के रूप में bus implementation inject की गई
    • test में single byte array आधारित Mock_bus से CPU instantiate किया गया
    • इससे CPU unit tests में असली bus की जगह mock implementation इस्तेमाल की जा सकी

instruction set और GADT का उपयोग

  • Game Boy instruction set में कुछ instructions 8-bit arguments लेते हैं और कुछ 16-bit arguments
    • ADD8 A, 0x12 8-bit A register और 8-bit immediate value को जोड़ता है
    • ADD16 AF, 0x1234 16-bit AF register और 16-bit immediate value को जोड़ता है
  • पहली कोशिश में arguments को Immediate8, Immediate16, R, RR जैसे variant से व्यक्त किया गया था
  • variant approach में read_arg का एकल return type तय करना कठिन था
    • R r से uint8 लौटता है
    • RR rr से uint16 लौटता है
    • एक ही match expression में return type अलग-अलग हो जाते हैं
  • GADT का उपयोग करके argument types फिर से परिभाषित किए गए
    • Immediate8 : uint8 -> uint8 arg
    • Immediate16 : uint16 -> uint16 arg
    • R : Registers.r -> uint8 arg
    • RR : Registers.rr -> uint16 arg
  • इस संरचना में read_arg : type a. a Instruction.arg -> a की तरह argument type के हिसाब से return type बदलता है
    • ADD8 केवल uint8 arg * uint8 arg लेता है
    • ADD16 केवल uint16 arg * uint16 arg लेता है
    • 8-bit और 16-bit instruction arguments के बीच भ्रम type level पर कम हो जाता है

cartridge और first-class modules

  • Game Boy cartridge केवल साधारण ROM नहीं होता; type के अनुसार उसमें अतिरिक्त hardware भी हो सकता है
  • ROM_ONLY type cartridge में केवल game data और code वाला ROM होता है
    • उदाहरण के लिए Tetris
  • MBC3 type cartridge में ROM के अलावा स्वतंत्र RAM और timer भी होते हैं
    • उदाहरण के लिए Pokémon Red
  • cartridge type के अनुसार features अलग होते हैं, इसलिए हर type को अलग module के रूप में implement किया गया
  • runtime पर सही cartridge type के अनुसार module चुनने के लिए first-class modules का उपयोग किया गया
    • Detect_cartridge.f को इस तरह डिज़ाइन किया गया कि वह ROM bytes लेकर (module Cartridge_intf.S) लौटाए

test ROM और ppx_expect आधारित integration testing

  • test ROM ऐसे programs होते हैं जो emulator की किसी विशेष functionality की जाँच करते हैं
    • basic arithmetic instructions का सही व्यवहार
    • MBC1 cartridge type support की जाँच
  • सामान्य game ROM के विपरीत, test ROM यह बता सकते हैं कि failure किस feature क्षेत्र में है, और कुछ core features के बिना भी चल सकते हैं; इसलिए emulator development में उपयोगी हैं
  • test ROM आमतौर पर test result screen पर दिखाते हैं
    • mooneye test ROMs failure पर register dump और assertion failure information दिखाते हैं
    • blargg test roms जैसे कुछ test ROM serial port से ASCII results output करते हैं
  • integration testing में ppx_expect का उपयोग किया गया
    • M.run_test_rom_and_print_framebuffer ROM चलाता है और अंतिम screen state को ASCII characters में print करता है
    • output string की तुलना [%expect{|...|}] के अंदर दिए गए expected value से की जाती है
    • ppx_expect की व्याख्या Jane Street के लेख में देखी जा सकती है
  • यह setup बड़े code changes के दौरान भी regression पकड़ लेता है और exploratory programming workflow को संभव बनाता है
    • किसी नई feature को validate करने वाला test ROM ढूँढना
    • उसके लिए ppx_expect test सेट करना
    • failed output को commit करना
    • feature implement करना
    • देखना कि test result Test OK में बदलता है या नहीं

JavaScript compilation और browser UI

  • js_of_ocaml की वजह से JavaScript compilation मुश्किल नहीं थी
  • emulator को browser में चलाने योग्य बनाने के लिए केवल एक single commit की ज़रूरत पड़ी
  • browser UI के लिए Brr का उपयोग किया गया
  • Brr, JS objects को OCaml objects के बजाय OCaml modules में map करता है
    • js_of_ocaml का built-in browser API, JS objects को OCaml objects में map करता है, इसलिए OCaml object system की जानकारी चाहिए
    • Brr का उपयोग OCaml object model की जटिलता कम करता है

performance optimization की प्रक्रिया

  • शुरुआती browser build काम कर रहा था, लेकिन खेलने लायक होने से बहुत धीमा था
    • PC browser पर लगभग 20 FPS मिल रहे थे
    • असली Game Boy 60 FPS पर चलता है, इसलिए प्रदर्शन लगभग 3 गुना बढ़ाना था
  • Chrome profiler से bottlenecks खोजे गए
    • GPU लगभग 73% समय ले रहा था
    • tile_data.ml 34%, oam_table.ml 18%, tile_map 8% समय ले रहे थे
    • timer.ml और कुछ Bigstringaf functions भी काफ़ी समय लेते थे
  • bottleneck हटाने से चरणबद्ध तरीके से FPS बढ़ा
  • इसके बाद PC browser पर 60 FPS मिल गए, लेकिन smartphone पर प्रदर्शन 20~40 FPS के बीच था
  • release build का JS output dev build से धीमा निकला, और discuss.ocaml.org की मदद से पता चला कि js_of_ocaml की inlining JS performance degradation का कारण थी
  • inlining बंद करने के बाद PC पर 100 FPS और smartphone पर 60 FPS हासिल हुए
  • JS performance optimization से native performance भी सुधरी, और native execution में लगभग 1000 FPS मिले

benchmark और comparison की सीमाएँ

  • UI के बिना emulator चलाने के लिए headless benchmarking mode implement किया गया
  • कई OCaml compiler backends पर FPS मापा गया
  • लेकिन यह benchmark दूसरे Game Boy emulators से FPS तुलना के लिए उपयुक्त नहीं है
    • emulator performance काफी हद तक accuracy और implemented feature scope पर निर्भर करती है
    • CAMLBOY में APU(Audio Processing Unit) implement नहीं किया गया, इसलिए APU support वाले emulators से FPS तुलना अर्थपूर्ण नहीं होगी

OCaml उपयोग का अनुभव

  • OCaml ecosystem पहले के लगभग 6 साल पुराने अनुभव की तुलना में काफ़ी बेहतर लगा
    • dune की वजह से files को directory में रखने भर से build system संभाल लेता है, ऐसा अनुभव मिलता है
    • Merlin और OCamlformat से autocomplete, code navigation और auto-formatting अपनाना अपेक्षाकृत आसान है
    • setup-ocaml से GitHub Actions में build और test setup किया जा सकता है
  • CAMLBOY implementation में performance कारणों से mutable state का बहुत उपयोग हुआ
    • कई modules में t -> ... -> unit type के functions हैं, जो किसी mutable state mutation को दर्शाते हैं
    • इतनी “non-functional” implementation होने पर भी OCaml के फ़ायदे कम नहीं लगे
  • पसंद का केंद्र “functional” होना नहीं, बल्कि static types, variants, pattern matching, module system और अच्छी type inference है

OCaml में असुविधाजनक पहलू

  • ecosystem बेहतर हुआ है, लेकिन कुछ क्षेत्र अभी भी जटिल हैं या उनकी documentation कमज़ोर है
    • reproducible तरीके से dependencies resolve करने की प्रक्रिया में official opam documentation में स्पष्ट मार्गदर्शन की कमी महसूस हुई
    • ज़रूरी commands खोजने के लिए setup-ocaml का source code पढ़ना पड़ा
    • किसी package को local तौर पर “publish” करके फिर उस local published package को install करना जटिल लगा
  • abstraction पर निर्भरता की syntactic cost अधिक है
    • अगर B को C की concrete implementation के बजाय C_intf interface पर निर्भर बनाना हो, तो B को functor में बदलना पड़ता है
    • B functor बनते ही A पहले की तरह B.foo refer नहीं कर सकता, इसलिए A को भी B_intf लेने वाला functor बनाना पड़ता है
    • किसी module को functor में बदलने से सिर्फ़ उसका दूसरे modules पर निर्भर होने का तरीका नहीं बदलता, बल्कि दूसरे modules का उस पर निर्भर होने का तरीका भी बदल जाता है
  • यह समस्या Camlboy -> Bus -> Cartridge dependency graph में Bus -> Cartridge हिस्से को अलग करने की कोशिश में सामने आई
  • OOP में class B का constructor यदि concrete class C की जगह interface C_intf ले, तो class B का type स्वयं नहीं बदलता
    • हालाँकि OOP में dynamic dispatch की cost होती है
    • और OCaml की OOP features से कम लोग परिचित होते हैं, जिससे code readership सीमित हो सकती है

संदर्भ सामग्री

  • OCaml संबंधित सामग्री
    • Learn OCaml Workshop: Jane Street के अंदर इस्तेमाल की गई workshop सामग्री, जिसमें holes वाले OCaml code और tests भरकर सीखा जाता है
    • Real World OCaml: OCaml की बुनियादी syntax जानने वालों या अन्य functional languages का अनुभव रखने वालों के लिए व्यावहारिक उदाहरण-आधारित सामग्री
  • Game Boy संबंधित सामग्री
    • The Ultimate Game Boy Talk: लगभग 1 घंटे में Game Boy की संरचना समझाने वाला वीडियो
    • gbops: Game Boy instruction set table
    • Game Boy CPU Manual: instruction implementation में इस्तेमाल किया गया CPU manual, हालांकि कुछ हिस्से, खासकर register flags के आसपास, पूरी तरह सही नहीं हैं
    • Pandocs: GPU, timer आदि hardware modules के व्यवहार के लिए संदर्भ wiki
    • Imran Nazar’s blog: JavaScript में Game Boy emulator बनाने की tutorial, जिसका उपयोग implementation scope का मोटा अंदाज़ा लेने के लिए किया गया

अभी कोई टिप्पणी नहीं है.

अभी कोई टिप्पणी नहीं है.