Bercriber's Blog

zigでゲームボーイエミュレータ書いてる

github/kmtoki/gbe-zig

エミュレータ開発用テストROMのgb_test_roms/cpu_instrsがパスしたのでcpuはざっくり書けたっぽい。過去にTypescriptとHaskellとRustで書いてて、TSはなんとか動いたけど、haskellとrustはcpuのテストをパスできなかったので三度目の正直ぎみ。

一番最初はrustで書き始めて、cpuの一命令ごとに関数呼び出しとかあかんやろとか適当なことを思って、命令を関数ではなくマクロで書いてしまったのでコンパイルがくっそ遅くなったのとエラーメッセージが読みにくくなったのでテストROMさえ回すことなく投げた。その次に、トライアンドエラーにおけるコンパイルのストレスをなくそうというのとブラウザで動かそうというモチベーションでTypescript/denoで書き始めた。低レイヤーの知識ゼロでなんにもわからない状態から始めたのと、既存エミュレータのコードは読まずに(daaで挫折してカンニングした)、英語読めないくせに英語ドキュメントしか見ないで実装するという謎の縛りプレイをやっていたので、数ヶ月かかりつつもなんとかブラウザで「ゼルダの伝説夢見る島」が動くようになった。その次に、haskellでとりあえずcpuざっくり書いたんだけど、コンパイル時間が10秒くらいかかるくせに、TS/deno版とくらべて4倍くらい遅いというかなしい結果になってしまって心折れた。lens/mtl/StateMonadでごりごりやっていったのがよくなかったのか、IOVectorが遅いのか、よくわからんけどちゃんと最適化すればどこまで早くなるのかは興味があるがコンパイルおそすぎて諦めてる。どこに最適化の余地があるのかもわからん。というかv8早すぎるだろ。

RustとかTSで書いたのが2年前で、Haskellで挫折したのが今年の夏で、なんかZig流行りだしたしこっちで試してみるかってなっていまここ。10日くらいでなんとなくcpu部分まで書けたんだけど、cpu_instrsテストをパスするまで一ヶ月かかってしまった。ここからこまかなタイミング合わせとか、ppuとか、apuとかやってるとまたどっかで詰んで終わらない感じになりそう。今回は詰んだら既存エミュのコードをカンニングしていくしかない。というかちょっと前に、tanakh先生が怒涛のエミュ実装祭りをやっていて、そのコードを見てしまったのでだいぶ影響されたコードになってしまった。良いことだろうけど。

zigで良かったところは、コンパイル早い、実行早い、黒魔術のないシンプルで素直な言語機能。便利だったのが、型が自明であれば、enumとかstructの名前を省略できる。`read(Register::IF)`と書かなければいけなさそうなところをread(.IF)と書けるのでありがたかった。おかげでinstructionの呼び出しが短く書けた。イマイチだったところはキャストがめんどい。@intCast(u16,0xff)と関数呼び出し的な文法で長ったらしい。演算子もデフォルトではオーバーフローするとランタイムエラーで落ちるので、GBEのケースでは255 + 1 // :u8255 +% 1と書く必要が多くあり、めんどい。あとはメモリ管理。人類には手動でメモリ管理するのは早すぎるとかなんとかは置いといて、ファイル一つ読むだけでも長くなる。

    pub fn readFile(allocator: std.mem.Allocator, path: []const u8) !ROM {
      const file = try std.fs.cwd().openFile(path, .{ .mode = .read_only });
      defer file.close();
      const stat = try file.stat();
      const buffer = try file.reader().readAllAlloc(allocator,stat.size);
      return parse(buffer);
    }


    pub fn main() !void {
      var gpa = std.heap.GeneralPurposeAllocator(.{}){};
      var path: []const u8 = "rom/gb_test_roms/cpu_instrs/cpu_instrs.gb";
      const rom = try ROM.readFile(gpa.allocator(), path);
    }

いちいちアロケーター引きずり回して開放も実装して(上記ではやってない)というのは、GCがないので当然ではあるし、その分軽くて早いという恩恵を受けるためにzig選んでいるわけだが、まぁめんどいよね。仕方がないというかなんというか。そのへんrustは上手だなと思うけど、使いこなすにはプログラム全体の構造をちゃんと把握して、所有権やライフタイムをきっちり理解した設計に最初からしないと破綻しがちというのは、クソザコナメクジの私にはつらい。場当たり的に書きながら考えるやり方なので、まぁしょうがないよね。