saino.me (kaishuu0123)

都内でひっそりと生きる IT エンジニアの個人ブログです

Go で実装したファミコンエミュレータ「ChibiNES」を公開しました

何番煎じか分かりませんが、私も「ファミコンエミュレータを書いてみたい!」と思って「ChibiNES」というファミコンエミュレータを公開しました

github.com

Releases からバイナリをダウンロードできます

ファミコンは子供の頃にやっていたこともあり、特段と思い入れがありました (とはいえ、私の世代はスーファミ全盛期なのだろうか)

機能紹介、苦労話、振り返りと雑多に書いていこうと思います

2022/08/01 追記

  • ToyNES から ChibiNES という名前に改名しました

機能紹介

README.md に貼り付けていますが、マリオや私の独断と偏見で選んだゲームが動きます。

NSF Player も(別コマンドでですが)動きます (音無しバージョン)

youtu.be

このプロジェクトを通じてやりたかったこと

  1. 自分の好きだったゲームを動かしたかった
    • 吸い出し機買いました
  2. マルチプラットフォーム(Windows, Mac, Linux)でバイナリを提供したい
    • マルチプラットフォームなライブラリの選定
    • GitHub Actions で Releases にバイナリが登録できるように
  3. ネイティブアプリケーションで動かしたい
    • ブラウザで動かす方法(Canvas, WebAudio)もありますが、そこはロマンです
  4. VirtuaNES に実装されている NSFプレイヤーのようなものを作りたい
  5. Go で何か重めな実装をしてみたかった

中身の話 (ライブラリとか選定理由とか)

ライブラリのお話

  • GUI 周りは go-gl/glfw (GLFW) と inkyblackness/imgui-go (Dear ImGUI)
    • Qt とか GTK (!?) とかも迷ったんですが、OS ネイティブな何かを使うわけではないので、Dear ImGUI で実装することにしました
    • Qt の OSS 版は M1 Mac というか Apple Silicon に対応していなかったので残念ながら却下となりました
    • GLFW 側で D&D に対応してくれていたので、ファイルダイアログの実装はしないことにしました
    • (余談) Mesen は Core 部分は C++、GUI 部分は C# (.NET Framework かな?) で書かれているみたいですね
  • 音声は portaudio (fogleman/nes から)
    • SDL2 の audio を使っても良かったのですが、SDL2 に移植するモチベは無かったのでそのままにしています

また、いろいろなところからコードを引っ張ってきてるので、Core 部分はキメラ化しています

  • Core 部分 (CPU, PPU, APU) は libretro/Mesen の丸パクリ を参考
  • NSF Player の再生機構は theinternetftw/famigo から
    • 表示部分は調べつつ書きました

エミュレータ実装を体験してみての感想

  • ファミコン文化すごい
    • 自作の ROM を開発していたり、チップチューンな曲を作っている方がいたり、モチベーションが凄いなぁ、と
    • NSD.lib などのライブラリを公開されている方もいらっしゃったり、奥が深い世界だなぁ、と感銘を受けています
  • ファミコン本体の拡張をせずに「カセットでどうにかしよう!」の気持ちが凄すぎる
    • 仕様理解として一番苦労したところなんですが、当時のファミコンゲーム開発者の方々の「とにかくカセットでなんとかしよう!」という情熱が凄いですね
    • バンク切り替えから始まり、拡張音源もあったり、チャレンジングすぎます
  • Mapper の各種実装が全然分からん
    • FPGA を使ったエミュレータとかであれば、カセットの中にチップが入ってるので Mapper の実装はいらないのですが、ソフトウェアエミュレーションではもちろん必要です
    • 「先人の方々はカセットをめちゃくちゃ解析して挙動を見たんだろうなぁ」と思い、その情熱に感服します
    • 全 Mapper 対応は趣味でやる領域じゃない 対応できてるソフトはすごい
  • 音のプログラミング奥が深すぎる
    • 未だに理解が浅いと思います
    • まず音の出し方から分からない、APU のコードを読んで「なんでこの音が出るんだ?」という疑問が晴れない、みたいなのを延々と繰り返してました
    • 小さいプログラムを書いて、サイン波を鳴らしてみたり、APU 周りはとにかく苦労しました
    • 拡張音源というものを知った時には「なんでやヽ(`Д´)ノ」と思わず叫びそうになったほど

一旦まとめ

  • 実際に仕様を読んでみたり、コードを書いてみないと知ることができないエミュレータの世界を知ることができました
  • ゲームボーイやスーパーファミコンのエミュレータはどうなってるんだろう?という気持ちが出てきました
    • Mapper ってやっぱりありそうな気がするけど、あるんかな。どれくらい複雑なんだろう・・・?
  • エミュレータ書きたいときはファミコンより仕様がシンプルなハードから始めたほうがいいかもしれない

あとは参考リンクと自分用の Twitter 振り返りを貼り付けておきます

興味がある方はどうぞ〜

参考リンク

www.nesdev.org

pgate1.at-ninja.jp

www.masswerk.at

振り返り

4 月中旬 fogleman/nes を Mapper 16 に対応させようと悪戦苦闘しているとき

fogleman/nes に Mapper 16 の実装ができて、セーブデータの保持のために EEPROM を書いていたころ

現実逃避でなぜか画面の拡大方法を検討しはじめる

この頃に nes-test-roms の存在に気づき、各種エミュレータの動作検証を開始

自分の好きだったゲームが動く

自分の作っているエミュレータの実装方針でまずいことに気づく (CPU と PPU のステップ実行の件)

ここらへんから Mesen の Core を Go に移植開始

ppu_vbl_nmi の rom が動く

自分でやりたいことがあったので imgui-go に PR を送る

NSF Player が一旦キリがいいところまで出来上がる

書籍: 6502とApple II システムROMの秘密 感想

6502とApple II システムROMの秘密

6502とApple II システムROMの秘密

  • 作者:柴田文彦
  • 発売日: 2020/02/25
  • メディア: 単行本(ソフトカバー)

一気読みした。

レトロコンピューティングが好きな私にとってはとても面白い内容だった。

ページ数が256ページではあるが、 とにかく内容がめちゃくちゃ濃い (256 ページというページ数にも何らかのこだわりがあるんだろうか?)

6502 の紹介からかなり力を入れていて、個人的には「第4章 6502のインストラクションセット」の各命令表に対して、実行サイクルが加筆されていた、という点が印象的だった。実行サイクルが書いてあると割と CPU の気持ちになりやすい気がする。

グラフィックに関する解説がとにかく力が入っていて、各種モードが詳細に解説されていた。もはや模造品を作れてもおかしくないレベル。

また、Apple Ⅱ (Apple ][) のハードウェア設計、ファームウェア、モニターコマンドにも触れていて、コンパクトかつ高機能な設計に驚かされた。

個人的にはサウンド周りの記述も期待したけど、そこはアッサリ気味の紹介だった。

Apple Ⅱ 上でプログラミングする側(ユーザ)の話なので、たしかにピックアップされると期待するのは変かも ... ?

インテルやモトローラなどの歴史的な関係も書かれており、様々な会社や人達が関わって、 6502 が生み出された、という経緯も丁寧に解説されていて、読みやすかった。

Apple Ⅱの話だから、「スティーブ・ウォズニアックの話が多いんだろうな」と勝手に想像していたが、彼自身の記述はまったくと言っていいほどなく、彼が生み出したプログラムを深く掘り下げて、その醍醐味を味わっていくような流れになっていた。人物像に焦点があたっていたら、集中して読めなかったかもしれない。

6502 と Apple Ⅱ に思い入れがある方の視点には立てない年齢ですが、「ハードウェアとソフトウェアがとても近い位置にいた頃は、こんなにも色々な工夫が隠されていたんだな。(分業だと恐らくここまでゴリゴリ密な実装にできない ... )」と痛感しました。

今では様々な規格が整備されたり、ハードウェアもデファクトスタンダードが決まりつつある今、過去にこういった尽力があった、という世界が垣間見えたのはとても楽しかったです (∩´∀`)∩

GROWI に draw.io 連携機能を PR (Pull Request)して v3.7.0-RC としてリリースされた話

Qiita からの転載です

「GROWI に draw.io 連携機能を PR (Pull Request)して v3.7.0-RC としてリリースされた話」

Qiita でいいねしていただけると嬉しいです(∩´∀`)∩


はじめに

GROWI に draw.io 連携機能を実装して Pull Request し、無事マージされて、バージョン 3.7.0 RC(Release Candidate) としてリリースされました!🎉 (今は docker image のみ提供されてます)

weseek/growi Add draw.io Integration #1685

使い方のドキュメントはこちらです。

draw.io で様々な図を作成する | GROWI Docs

この記事では簡単な機能紹介と実装のモチベーション、どうやって実装したかなどをお話しようと思います。

GROWI って何?

株式会社 WESEEK が OSS として開発している Markdown で書ける Wiki です。

技術スタックは下記の通り、わりと鉄板な構成です

  • React
  • webpack
  • Express
  • MongoDB
  • etc ...

どういうことができるようになったの?

PR に掲載しているアニメーション GIF を置いておきます。

75884338-f7304680-5e67-11ea-929d-b7614b8d671a.gif

端的に言うと下記の項目が一通りできるようになりました。

  1. 編集画面から、ツールバーの draw.io アイコンをクリックすることで図を追加できる
  2. 追加した draw.io の図がページ表示画面で表示できる
  3. draw.io の図を2通りの方法で編集できる
    • ページ表示画面で「編集」ボタンから図の編集ができる
    • 編集画面で画面左側にあるエディタ上で Base64 エンコードされた箇所にカーソルを合わせて、ツールバーの draw.io アイコンをクリックすることで既存の図が編集できる

作ったモチベーション

GROWI には元々 PlantUML で UML 図を描画できる機能blockdiag で図を描画できる機能 が搭載されていました。

「これでめっちゃドキュメント書き放題やんけ!」と思っていたのですが、やはり記法を覚えないといけなかったり、自分で図のカスタムを行いたいときの不自由さが目につきました。

また、 Twitter や Qiita で GROWI の反応を見ると、作図機能を使おうと試行錯誤してくれている方は結構居るものの「使いづらいかもしれん ... (´・ω・`)」という気持ちが湧きました。といいつつ、blockdiag の図を生成する機能を追加したのも自分なんですが。 (PR はこちら)

アレコレ考えるうちに、「図はやっぱり見たまま編集できる (WYSIWYG) 方法が一番だろ!」という至極シンプルな理由で実装することを決めました。

また、実装中何度も心が折れかけましたが、 Qiita で draw.io の使い方の解説記事が公開されて「これは実装しきるしかない!」と後押しされた、というのも最後まで完成させる大きなモチベーションになりました。

また、 draw.io の図が GROWI で管理できるようになると、ページの「更新履歴」タブから過去の図を復元できるため、GROWI の仕組みを上手く活かせるのも嬉しいポイントですね 🍀

完成に至るまでの道のり

ここからはどのように実装していったかの経緯を書いておきます。

1. draw.io の図をテキスト形式で表現できないか調べる

GROWI の編集画面は WYSIWYG 形式ではなく、テキスト形式を想定していますので、Markdown 内にバイナリデータを埋め込もうとしても、その表現方法がありません。 そのため、テキストで埋め込む形式をまず調べる必要があります。Google さんにお尋ねしてもあまりいい答えは返ってこなかったため、 draw.io を提供している jgraph の github を調べることにしました。

そこで、jgraph/drawio-integration という目的に合ったものが見つかったため、このソースコードを見つつ、テキスト形式で表現できることが分かりました。

コードでいうと ここらへん

大分ざっくり話しますが、バイナリ形式を Base64 化した形式で表現していることが分かります。

2. draw.io の仕組みについて調べる

draw.io の図のサンプルを見ながら、データ形式を探っていきます。

さらにデータ形式を解釈して図として変換してくれる JS が draw.io のコードのどこに該当するかを見つけるために、コードを読み進めていきます。 GraphViewer.js というものがデータを解釈して、図として変換してくれることが分かったため、この JS を使ってプレビューを行うことが分かりました。

実際にビルドされたソースは viewer.min.js という最適化された形の JS です。

ここまで分かれば「特定の HTML タグに Base64 化されたデータを埋め込んで、 viewer.min.js に食わせれば 表示ができる」というところまで到達できます。(実際もっと途方も無い時間がかかってますが)

その後に小さなプロトタイプを HTML と JS で作り、自分の仮定が上手く行っていることを確認しました。

3. markdown-it が draw.io のデータを食って、特定のタグを吐けるような npm パッケージ を作る

プロトタイプ実装後、 GROWI にすべての担当をお任せすることはコード量が増えることを懸念し、 「draw.io のデータを食わせると、特定のタグを吐かせる markdown-it のプラグインを担当」する npm パッケージを作りました。かなり端折ってるので、詳しく知りたい方は質問していただけると。

入力

::: drawio
(ここに draw.io のデータ(Base64 エンコードされたもの)が入る)
:::

出力

<div class="drawio-viewer-index-0 markdownItDrawioViewer"
  data-begin-line-number-of-markdown="2"
  data-end-line-number-of-markdown="6">
  <div class="mxgraph" style="max-width: 100%; border: 1px solid transparent" data-mxgraph="{&quot;editable&quot;:false,&quot;highlight&quot;:&quot;#0000ff&quot;,&quot;nav&quot;:false,&quot;toolbar&quot;:null,&quot;edit&quot;:null,&quot;resize&quot;:true,&quot;lightbox&quot;:&quot;open&quot;,&quot;xml&quot;:&quot;&lt;?xml version=\&quot;1.0\&quot; encoding=\&quot;UTF-8\&quot;?&gt;\n&lt;mxfile userAgent=\&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36\&quot; version=\&quot;6.8.9\&quot; editor=\&quot;www.draw.io\&quot; type=\&quot;atlas\&quot;&gt;&lt;mxAtlasLibraries/&gt;&lt;diagram name=\&quot;Page-1\&quot; id=\&quot;bac0fdb4-bee4-c619-222c-02e826218fb6\&quot;&gt;1VtNk5s4EP01vqYQAhsfY2eS3UMqqfJu7eaowQooAeQI4Y/99SsGgXELJw7GoPHBhRrRkl4/ulste4bX6fGDILv4I9/SZOY62+MMv5u5brBcqO9ScKoEfhBUgkiwbSVCZ8GG/Ue10NHSgm1pftFRcp5ItrsUhjzLaCgvZEQIfrjs9pUnl6PuSKRHdM6CTUgSanT7h21lrJflt3r/QVkU1yMjR995JuH3SPAi0+PNXPz15VPdTkmtS/fPY7Llh5YIP83wWnAuq6v0uKZJCW0NW/Xc+yt3m3kLmslbHnCrB/YkKfTS3zESCZLq6clTDckhZpJudiQs2wdl9Rle5VLw7w1CWElimSbqEqlLcyp6dnsqJD22RHpqHyhPqRQn1UXf9eYaJk0jTzcPZ5ugGsm4ZY/6MaJpEDWaz1CoC41GNzLYQGYjRRHKQtCZO1d2VVzFK5vQat4ejRYaEy7PgGtFY7JnvBCWwhVMCZdvwLVOSJ4rkU0QLS8Rcv0REZqbCPF0x3OFQotQtr+TgGM4GBHBhYHgZxWdyshnJ1YYTwhWYAZCukv4KS3XYydeaDEhXsvu1zOzF67Ge03hzWo/0MLr0/O3MnW1EywPTQkWMsB6G0q2Z/JkKVx+MCVcZhL/ZyapIAoznlmK2AJPiZiZ3P+dq7DorElua3QMJqWYmd5vJCkzMecjCWOW2YoaciblmZnmb+iPgmahrXhBRzZqToE6c/60yFhIXpErGxczM8vvdv6f1IL2jB4sBRG6t3FBNLP/v1jKsshSsAyvNi5aZu5voEK3Ed3oJk2e+eHpLFi9CNSNcsHq1VbjrF5KprQcwLkRrZwXItSj6XAuiYio7qWnVM7jp4i2AXM6AGuEgibKCe0vC8RdMOoxPnNWboWulFKavVutoVqOfuhsC0MPAoqgngoDQ8+LTZtl32Tmmj72mNmz38w+LJm5Pe08h4SBigY0tLndmtjQvmlobJmhYVqJ+77PQBGCiq7Y+a0Q5NTqtis75Nfni2D14/JgSF1UGnuTyNyE3k0iemTy35JCb3zd+qIJpcgjTq1bZfOLZth91Fu+Ouo15xb3Ug/qGYh5xinC0MwzN/MTu6+6dmYzifyh8hG/Vz7yuyTCiweTyCxwTEyiuf0cgo5oOZAjgnqGCoGPdkRmtedVhsDAfuYBS/q9mQcUeQ+innH2NDT1zMrZ/dT7CcGu0/I+6i3spx50erCi0tfpQT1DBU7vwcwz648TB8769LBNIs8yEvngh21u0JNFc6AIQUUD0WgBJzw0jcwK7NQ06qg22UYjD54J9o2D8NjHSOUGotESTnhoGj2gND1FHKyP9G0mn3/tZPW3fRgMqVDRQOQL4IQHJl+t/tWTr+M4xTryOQN5vrkzkueDP6gamnzWFfFRRxW/9mvW8MiDO8m+Tgwe/Hg3OrE+pnatM3VHsco6U89B7uH33bktgCLvxq1bH1PbV9vu2KLbZ2o0lKnReKa2rgKNOgqB1pka+l2/904aOvAbd9K/NrVqnv/QV3U//2kSP/0P&lt;/diagram&gt;&lt;/mxfile&gt;&quot;}"></div>
</div>

4. GROWI に実装

この実装を行う際には GROWI のメンテナである @yuki-takei さんに GROWI でのレンダリングの仕組みなどを教えていただきつつ、実装を行いました。ありがとうございます ✨

  • draw.io エディタの起動方法を調べて、iframe 内で draw.io エディタを表示して position: fixed で既存画面に被るように表示し、 iframe と GROWI 間でデータが受け渡せるように実装したり
  • 不用意に再レンダリングが走らないように GROWI の lsx プラグインなどと同様に draw.io の図を React コンポーネント化したり
  • Table 編集機能と同じようにページ表示部分から「編集」ボタンを押すことで、図の更新ができるようにしたり
  • 既存の仕組みに乗っかるようなコード構成に大修正したり

と GROWI に組み込む際にもそこそこのコード量になりました。( PR の内容 )

アドバイスもあり、とてもシンプルな形で機能を追加できたんじゃないかなと思っています。

今後の課題

  1. やっぱり図が大きくなればなるほど、レンダリングが遅くなるんじゃないか、という不安
    • これは実際に使ってみてもらってから様子を見ればいいかな、と思っています
    • 最悪、図を他のページに分割して保存するという方法で回避していただく方向で ...
  2. エディタ画面に Base64 エンコードした文字列がそのまま書かれるのは分かりづらい
    • これはエディタの折りたたみ機能を使って、なるべく Base64 エンコードされた文字列が意識されないようにできるといいかなぁと思っています。
    • 本当は添付ファイルを CRUD できる機能が GROWI にあれば、 draw.io の図は画像みたいな取り扱いができるっちゃできるんですが、それはそれで実装コストが高すぎる気がするので

最後に

draw.io の仕組みを理解するまでに結構な時間がかかりましたが、 Markdown に draw.io の図を埋め込める機能が作れた経験は貴重でした 🌟 また、 draw.io に関する様々なノウハウを得ることができました。(データ構造や viewer.min.js の仕組みなどは今回の記事ではめちゃくちゃ端折っています)

Visual Studio Code の拡張機能に応用できたりするかもしれません。

皆さんも是非今回実装した機能を使ってみてください 🌟

Go 言語の練習を兼ねて SDL2 でライフゲームを作ってみました。

作ろうと思った動機

www.saino.me

上記エントリで CHIP-8 のエミュレータを作ろうと思っていて、ディスプレイを擬似的に用意しようと思ったときに「GUI でドットを表現する」ということを基礎として学びたかったというのが1つ。

また、Go のチュートリアルは一通りやってみましたが、やはり何か「やってる感」があるプログラムを採用したかったためです。

SDL2 自体も色々とできて、なかなか面白そうだったので、グラフィック周りのハンドリングも学べるということで、一石二鳥ということでやってみました。

たぶん普通は CUI プログラムを書いて、画面を逐一クリアしてターミナルに逐一表示するほうが簡単だと思いますが、そこはロマンということで ...

作ったもののデモ

こちらは初期位置をランダムに配置したバージョンです。マスをクリックすることで、強制的にマスを「生きている状態」にすることができます。

ランダムバージョン

また配置をソースコードで記述してあげることで、こういった規則的なアニメーションができます。プログラムでこういったアニメーションが描画ができるなんて、かっこいいです( *ˊᵕˋ ) ⁾⁾

Cross バージョン

ソースコード

github.com

作り切るまでの過程

せっかく作ったので、足跡を残すために「どのように順を追って勉強をしたか」という点を残しておこうと思います。

1. Go について学ぶ

こちらは、有名すぎる「A Tour of Go」を一通りなぞりました。

go-tour-jp.appspot.com

2. Go の開発環境について学ぶ

私が Go を始めたころは Go Modules というものがなかった頃の話なので、アップデートがてら、Go Modules を使った開発環境を調べました。下記サイトでは goenv を利用していますが、私は brew で Go 1.12.5 を導入しました。

rightcode.co.jp

3. グラフィックライブラリの選定

ここは散々迷いました。もともとグラフィックライブラリには精通していないので、「OpenGL というものがあったり、Windows では DirectX ってのがあるな ...」程度にしか理解していません。

なので、記事先頭で引用した記事内で SDL2 を利用しているということなので、SDL2 を使うことにしました。GLUT や GLFW などもありますが、Go の binding では pixel というものもあるようです。

github.com

なかなか奥が深いです。ライブラリの棲み分けのブロック図とかないのかな、これ。

というわけで SDL2 を採用することを決めたので、Go の SDL2 binding である下記のveandco/go-sdl2 というライブラリを利用することにしました。

github.com

4. SDL2 の使い方を学ぶ

SDL2 を使うことにしたので、早速「ウインドウの表示」と「ピクセル単位での描画」を学んでいきます。

ここでは、「SDL には Window, Renderer, Texture」というものがあるんだな、というざっくりとした理解をして、「Texture に特定のデータの配列を渡して、Texture のデータを Renderer にコピーすればそれっぽいものを描画するんだな」ということを理解しておきます。

上記のソースでは sdl2canvas という自分が定義したパッケージに pixels という配列を持たせて、そこに R8G8B8A8 形式でデータを持たせて、それを Texture に渡すことでピクセル単位の描画を行っています。

github.com

5. ライフゲームのルールを把握して実装する

SDL2 とはまったく関係なく、ライフゲームのルールを作り込んでいきます。

github.com

ライフゲームでは、フィールドがあり、ステップを進めるたびに特定のルールで次々とフィールド上で生死を繰り返していきます。

ここでは、「現在のフィールド(Current)」と「現在のフィールドを元に生死判定を行ったあとのフィールド(Next)」の2つのフィールドを持っておきます。生死判定が終わったあとに「現在のフィールド」にコピーします。

ちなみに生死判定はいろいろなルールがあるそうですね。

ja.wikipedia.org

ちなみに正式名称は「Conway's Game of Life」と言うそうで、ボードゲームと名前がかぶるから「Conway's」という名前が先頭についているようです。

ja.wikipedia.org

6. ライフゲームのフィールドを SDL2 で作ったウインドウに反映する

私が書いたコードの該当箇所はこちら

ライフゲームが持っている配列から、SDL2 Texture に反映する pixels という配列に反映します。

ここでは、ウインドウの横幅 -> 縦幅の順で for 文を回し、特定の範囲ごとに色を塗り分けています。

これで完成です⸜( ´ ꒳ ` )⸝

まとめ

SDL2 を使ってウィンドウを表示して、ドット絵っぽいような画面を描画して、そこにライフゲームのロジックを組み込むということができました。

これで、何らかのエミュレータを描くこともできるようになったかなと思います。

CHIP-8 エミュレータ書くぞー

ライフゲームに関する蛇足

ライフゲーム自体も奥が深く、専用の wiki があり、パターンファイルというものまで決まっているそうです。

www.conwaylife.com

RTE というファイルフォーマットがあり、このファイルにはルールやドットパターンなどが記述されています。このファイルを読み込んで、描画することで特定のアニメーションを再生できる、という仕組みですね。

ここまで対応できると、すでに先人が見つけたパターンを試すことができるので、これはこれでまた1つ面白いトピックだと思います。(今回は RTE ファイルの読み込みまではやっていません)