セキュリティ系の勉強、その他開発メモとか雑談. GithubはUnity触っていた頃ものがメイン Twitterフォローもよろしくです

ファミコンエミュレータを写経してみるお話3【HelloWorld(sample1.nes)の実行】

前回

ファミコンエミュレータを写経してみるお話2【PPU】 - 196Log

やること

前回はPPUの機能を実装しましたが、まだ動作確認ができていないので、CPUと合わせてテストROMが動くようにしようと思います。まずはこちらのsample1.nesを起動させてみます。hello word!と表示されるものです。他のromはおそらく動きません。 f:id:thinline196:20200309165322p:plain

NES研究室 - サンプル


参考にするもの

  • コード周りはこちらのコードを観させてもらおうと思います。またRustの書き方の勉強としても頼りにさせてもらいます。
    github.com

ファミコンエミュレータの創り方 - Speaker Deck

  • こちらに有志の皆さんがNESの情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。

hp.vector.co.jp

pgate1.at-ninja.jp

  • こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。

booth.pm

読み始める前に

  • ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語はRustを使っており参考サイトもRustが中心のものが多いです。
  • また、解説力も乏しいので、適宜実装が完了したと思われるcommitにジャンプできるGithubのリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。

修正

sample1.nesを呼び出すために修正した箇所があります。まだ修正してない場合は参照してください。

【Fix】Hello world bug fix by 196Ikuchil · Pull Request #7 · 196Ikuchil/nes_emulator · GitHub


ひとまず呼び出そうとしてみる

現状頑張って機能を使おうとした場合、何が足りないのか把握します。下のリンクは軽い修正も含めて今までのものを実装したものになります。

github.com

  • rendererという生成したスプライトを描画する機能
  • カセットを読み込む機能(ROM, parse, Cassette
  • 上を操るjs側の実装全般

カセットのデータ

Busも通さなければいけないので、最初にカセットの読み込みを実装するのが良さそうです。下のリンクではカセットデータをRomへ流し込み、エミュレータContext内で扱えるように実装しなおしています。コミット履歴から、カセットのパーサーやRomの実装についても確認可能です。

github.com


描画担当

次はrendererを実装します。これは描画を担当する部分です。PPUで生成したデータを取り扱ってくれるようにします。

github.com

環境を整える

Rustの成果物をjsから呼び出して、ブラウザ上で動作させるようにします。npmによって入れるものはローカルで成果物を走らせてブラウザで表示するために使用します。main.rsには、js側から呼び出すrunメソッドを実装します。Makefileを作りmakeでビルド等を走らせます。.cargo/configに、ビルドに必要な設定を記述しておきましょう。

ビルドの際に必要になるものは以下なので、PCにインストールしておいてください。

- `nodejs`
- `cmake`
- `emscripten`(emcc)
- `rustup`

externsにはjs側が公開しているメソッドをrust側から呼び出すためのことを記述しています。起動実行にはemscriptenが用意したメソッドを、描画にはcanvas_renderというメソッドを呼び出せるようにしています。
最終的にプルリクにまとめているので、こちらを見た方が早いかもしれません。

github.com

sample1.nesは起動しますが、他はまだ起動しないでしょう。

デバッグに関して

起動しない場合、まずCPUの実装を疑うべきですが、初回はbackgroundの値を出力してみるべきと思います。それにより描画部分かそれ以前かに分けられるので、以前であればtileあたりのデータを覗いてみましょう。

そこにもデータが上手く反映されていなければ、CPUの実装ミスを疑います。特に今回のROMは短いので、バイナリエディタと合わせて、実装されるopecodeを追っていくことで、CPUの実装ミスに気がつけると思います。 <br> それがダメであればPPUの実装をみますが、この辺りは単独では大変なので、僕は参考コードと自分の実装、同じ位置にprintln!`をおき、値をくらべながら違う値が扱われている箇所を探しました。実装がほぼ同じであれば、意外と有効な技なので、試してみてください。

次回

次回はデバッグを行うためにnestest.nesを起動させ(るためにデバッグを行い)ます。

ファミコンエミュレータを写経してみるお話2【PPU】

前回

thinline196.hatenablog.com

やること

前回は初歩として、CPUの実装を行い、参考サイトを元に一通りの必要と思われる命令を網羅的に実装しました。まだファミコンをエミュレートする際に必要な実装がかけているかもしれませんが、その場合は後々修正していこうと思っています。

LogiClover vol.1を読んでいると、CPUの次にPPUを実装しています。これは、ファミコンの描画を司る部分だそうで、CPUにより間接的に制御されているとのこと。今回は制御をする機構はなるべく後回しに、PPUスタンドアロンなトコから実装していけたらなと思ってます。

参考にするもの

  • コード周りはこちらのコードを観させてもらおうと思います。またRustの書き方の勉強としても頼りにさせてもらいます。 github.com

ファミコンエミュレータの創り方 - Speaker Deck

  • こちらに有志の皆さんがNESの情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。

hp.vector.co.jp

pgate1.at-ninja.jp

  • こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。

booth.pm

読み始める前に

  • ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない)用語の説明もほとんどしていません。 しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語はRustを使っており参考サイトもRustが中心のものが多いです。
  • また、解説力も乏しいので、適宜実装が完了したと思われるcommitにジャンプできるGithubのリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。

レジスタ

前回のCPU同様、レジスタ周りから作り始めるのが良さそうです。ということで、コード参考のリポジトリを確認すると、どうやらRAMの実装とOAMPPU_ADDRなどを別途細かく分けて実装していること、またそれらにRAMの実装が必要なことが分かったので同時にやってしまいます。RAMの実装は、いろんな箇所で使いまわせるような実装になっていました。Registerの入出力周りのベースを実装したのがこのコミットです。

github.com

現状ここの実装が分からなかったので、修正しました。今後の動きによっては元に戻そうと思います。 https://github.com/196Ikuchil/nes_emulator/commit/5c3f2690437d9552b7af515fb047210fc8aaa309#r37678779

PPU

レジスタが動きそうなので、これを利用してPPUの動きをエミュレートしていきます。まず描画する背景、スプライトを生成できるようにしようと思います。参考書籍より、タイルが最小構成要素だと思ったのですが、参考実装ではsprite_utils.rsSpriteという構造体がおそらく最小要素になっている(タイル・スプライトの共通要素をここで定義)ので、そこから作ります。

注意点として以下があります。

  • 8*16のスプライトに対応済みとのことなのでそのまま実装する
  • Mapper3に対応済みとのことだが、現状不必要なので実装しない

PPUを呼び出せる部分まで実装しました。これをフレームごとに呼び出して画面を作っていくイメージです。リンクはPPUメイン実装時のコミットですが、パーツごとになんとなくコミットを分けているのでそちらも実装順の参考になるかもです。

github.com

runさせることで、そのフレームで必要な背景とスプライトを一通り生成するはずです。

DMA

CPUを介さずに直接RAMへデータを読み込む機構を実装します。詳しくはこのスライドに紹介されています。
ファミコンエミュレータの創り方 - Speaker Deck

これも機能のみ実装しておきます。

github.com

後々に、Bus0x4014番地に書き込みがあった場合にDMAによる転送が有効になるように仕込みますが、現状は実装されていません。

積み残し

今回は最低限PPUの機能として必要そうな部分のみの実装を行いました。テストコードをサボってしまい少ない分、しっかり動くかまだ未知数ですが、切りがいいので続きは次回に回します。次回は前回のCPUと合わせてテストROMを動かせるようにして、今回の分のデバッグも行いたいと考えています。

追記

修正が加わっているので、PPUあたりの該当コードは直しておいてください。後々修正する記事で同じリンクが登場するので今じゃなくても大丈夫です。

github.com github.com
ここではspriteに修正に入っています。

github.com
またここでは画面のスクロールに修正が入っています。

github.com

ファミコンエミュレータを写経してみるお話1【CPU】

はじめに

タイトル通りです。不定期に進めるつもりですが、最後まで進むかはまだ分からないです。


モチベーション

なぜ突然始めようと思ったか。 - 自分で調べてみたいゲームがある(秘密) - 低レイヤーに強くなりたい - Rustに興味がある

巷では自作OSが流行っていますが、みんなと同じだと芸がないので若干遠いものを選んでみました。

参考にするもの

  • コード周りはこちらのコードを観させてもらおうと思います。またRustの書き方の勉強としても頼りにさせてもらいます。

github.com

ファミコンエミュレータの創り方 - Speaker Deck

  • こちらに有志の皆さんがNESの情報をまとめて下さっているので、利用させてもらいます。特に、コードを写経しているだけでは意味がないので、まずこちらを見て自分で実装を考えます。

hp.vector.co.jp

pgate1.at-ninja.jp

  • こちらの本も参考にさせてもらいます。1つ目のリンクの方の実装と見比べる事で、実装にマストな部分を見出すために使いました。また、こちらの方の実装順番も参考にしてます。

booth.pm

読み始める前に

  • ファミコンの仕様についてはネット上にたくさん情報が上がっているので、ここでは詳しい事は書かないです。(自分も分からない) しかし、実装手順の再現性があるサイトは個人的に少ないなと感じたので、自分の試行錯誤をここにまとめて、うまく踏み台にしてもらえたらなと考えています。言語はRustを使っており参考サイトもRustが中心のものが多いです。
  • また、解説力も乏しいので、適宜実装が完了したと思われるcommitにジャンプできるGithubのリンクをそれぞれに置いておくので、そこからコミット履歴などを参照していただいてもらう形にします。申し訳ないです。

始めにやること

まず、ファミコンを構成する全体像を把握していくと良いと思います。CPUPPUがどうつながっているのかなどなど。

ファミコンエミュレータの創り方 - Speaker Deck

色んなサイトがそれぞれの図で説明しているので、目を通した方が掴みやすいかもしれません。

CPUから作り始めてみる

僕はCPUから作り始めてみることにしました。理由は以下です。

  • ここの命令を駆使してコンピュータが動いている
  • 割と他の機器に依存しておらず、コードが読み始めやすい
  • 同じ理由で、とりあえず命令が1つずつ動くように作れば大丈夫そう


命令を1つずつ実装する

実装が必要なCPUの命令は参考サイトに一覧で載っているので、簡単に言えばこれらを呼び出して動くようにできればokなはず。まず、大まかな流れを作りながら、LDA imm関数(中身未実装)が呼べるまでを作ってみました。(リンク先はその時のコミットに飛びます)

github.com

ここでcargo runを実行すれば以下のような出力が得られ、とりあえず呼び出せた事がわかりました。

f:id:thinline196:20200228012740p:plain

足りないもの

命令だけ実装していくつもりでしたが、CPU レジスタBusまわりの実装も必要そうだと感じてきました。命令による状態の変化であったり、アドレスの指す他の場所のデータを読んでくるといった処理を命令に実装しなければならないためです。特にレジスタ周りはプログラムカウンタなど特に不可欠な要素が多いので、命令系よりも先に実装しておく必要がありそうです。

レジスタの実装

簡易的に必要となりうる機能を参考サイト様から引っ張って実装してみました。実際に操作はしていないものの、操作を呼び出せるようにCPUに接続してみたつもりです。 github.com
テストを走らせるために、データを他とやり取りするbusも必要そうです。テストだけであれば細かな実装はいらなそうなので、うまいことモックテストができるように実装してみます。

busの仮実装と初めての命令の実装

はじめに仮実装としてBusと思われるものを実装しました。機能は適当ですが、読み書きができるメソッドが呼び出し可能なようにしておき、周辺が充実してきたら機能を実装してく形にします。

github.com


実際にBusをCPUのなかに組み込み、LDA imm命令を実装、テストを追加してみたのもがこちらです。これだとまだモックは必要ありませんでした。

github.com

LDA系の命令を一通り実装する

だんだん説明が少なってますが、こちらがLDA系を網羅した際のコミットです。それぞれある程度のテストも実装しています。modでのテストはBusにより実際に命令を読んできて、その命令を実行させるという、大分本番に近い感じで動いていると思います。楽しいです。

はじめはLDAの命令を網羅的に実装していたのですが、「読んだ値をそのままAへロード」か「読んだアドレスの指す場所の値をAへロード」の2パターンしかないので、結果的にInstructionには2つしかメソッドが実装されていません。

github.com

全ての命令を網羅する

あとは全ての実装をしていくだけです。基本的に下の2つのサイトを確認しながら、命令の挙動を把握して行きます。

hp.vector.co.jp

pgate1.at-ninja.jp
一通り網羅しました。これにより基本的なCPU周りの実装はひとまず完了したと思われるので、プルリクにしてあります。

github.com


割り込み系の追加実装

上ではソフトウェアからのBRKの割り込みは実装したはずですが、ハードウェアからなどの割り込みは未実装でしたので追加します。下のリンク先では、追加実装時のプルリクを紹介しています。

github.com


ここまで

一通りテストも書いたはずなので、ポカはないと思いますが、間違っているとしたら自分の解釈部分で違っていた故のミスかと思います。ひとまずCPUの実装(写経)はこれで一段落つけます。もし追加で必要なものや修正があれば、後の実装で適宜直していこうと思います。このエントリがいつまで続くか分からないですが、引き続き頑張ります。
つづき

thinline196.hatenablog.com

追記

後日romを起動させながらデバッグを行い修正が加わったので、CPUの該当コードあたりは直しておいてください。修正する記事で後々出てくるコードなので今じゃなくても大丈夫です。

github.com

github.com

Signed Int加算減算のオーバーフロー判定について

はじめに

Rustファミコンエミュレータを写経していた時のお話です。CPUの実装で加算・減算の命令実装時に、オーバーフローの判定を挟むのですが何やっているのか分からず悩んだので、ここに自分なりの解釈を書いておきます。もっと良い考え方があれば教えてください。

問題の箇所

このリポジトリのこの箇所です。

pub fn adc_imm<T: CpuRegisters>(operand: Word, registers: &mut T) {
    let computed = (operand as u16) + registers.get_A() as u16 +
                   bool_to_u8(registers.get_carry()) as u16;
    let acc = registers.get_A();
    registers
        .set_overflow(!(((acc ^ (operand as Data)) & 0x80) != 0) &&
                      (((acc ^ computed as Data) & 0x80)) != 0) //  ココ
        .update_negative_by(computed as Data)
        .update_zero_by(computed as Data)
        .set_carry(computed > 0xFF)
        .set_A(computed as Data);
}

// [引用] https://github.com/bokuweb/rustynes/blob/f213881554e20054c7ea7adafe511195c25f8cb7/src/nes/cpu/instructions.rs#L147

github.com


前提知識

ざっくりまとめると

  • ファミコンでは演算時に値をSigned Intとして扱っているはずである。
  • このメソッドはu8operandaccの2つの変数の足し算。(正確には1か0のcarry_flagも足している)
  • ファミコンにおいてもし結果がオーバーフローしていたらそれを検知する必要がある。
  • !(((acc ^ (operand as Data)) & 0x80) != 0) && (((acc ^ computed as Data) & 0x80)) != 0trueなら、オーバーフローしてる判定らしい


オーバーフロー検知

分解

まず判定式を2つに分けてみます。

  1. !(((acc ^ (operand as Data)) & 0x80) != 0)
  2. (((acc ^ computed as Data) & 0x80)) != 0

1では1つ目の変数と2つ目の変数をxorした後に、0x80ANDを取っています。よく分からないので更に分解してみます。xorとandで分配則が成り立つはずなので、分解します。ついでに先頭の!も取ります。
- (acc & 0x80) ^ (operand & 0x80)==0

http://markun.cs.shinshu-u.ac.jp/learn/logic/logic3/html/jp/fnd4-j.html

判定

  • 0x80andをとることで、8bit目が1かどうか判定してます。これにより値の正負を判定できます。(正なら0x80, 負なら0x00)
  • 上の結果をxorすることにより、元の2変数の正負が同じであった場合のみ0が算出されます。(正:0x80 ^ 負:0x00 = 0x80, 正^正= 0x00)
  • つまり1では、足した2変数が同じ符号であったかを見てます。



これらを踏まえると2では、変数1と演算結果が違う値かを判定していることになると思います。

つまり?

足し算なのに結果の正負が変わることはないよ。変わっていたら、お前オーバーフローしてね?ってことだと思います。 f:id:thinline196:20200301184235p:plain

減算も見てみる

手短に引き算も見ます。

pub fn sbc_imm<T: CpuRegisters>(operand: Word, registers: &mut T) {
    let computed = registers.get_A() as i16 - (operand as i16) -
                   bool_to_u8(!registers.get_carry()) as i16;
    let acc = registers.get_A();
    registers
        .set_overflow((((acc ^ (operand as Data)) & 0x80) != 0) &&
                      (((acc ^ computed as Data) & 0x80)) != 0) // ココ
        .update_negative_by(computed as Data)
        .update_zero_by(computed as Data)
        .set_carry(computed >= 0 as i16)
        .set_A(computed as Data);
}

// 引用: https://github.com/bokuweb/rustynes/blob/f213881554e20054c7ea7adafe511195c25f8cb7/src/nes/cpu/instructions.rs#L174

rustynes/instructions.rs at f213881554e20054c7ea7adafe511195c25f8cb7 · bokuweb/rustynes · GitHub


日本語に直すと、変数1と変数2の符号が違っているのに変数1と演算結果も符号が違うとなりそうです。これは式が

  • A - (B) = C

であることに注意すれば、足し算と同じように理解できそうです。

【Docker】docker-entrypoint-initdb.d内ファイルっていつ呼ばれるのよ

タイトル通り

初歩的な内容です。いつ呼ばれるというのは、"タイミング"の話ではなく、"初回起動時"とかそんな感じです。

docker-compose.yml

大体こんな感じの設定でビルドしてます。

#docker-compose.yml
version: '3'
services:
...
   db:
    image: mysql:8.0
    container_name: mysql
    command: mysqld --sql_mode=""
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - "./sql:/docker-entrypoint-initdb.d"
      - "./mysql_data:/var/lib/mysql"


たどる

mysql8.0ではdocker-entrypoint.sh内の次の行にてファイルが呼ばれている。

# docker-entrypoint.sh
for f in /docker-entrypoint-initdb.d/*; do
    process_init_file "$f" "${mysql[@]}"
done

# https://github.com/docker-library/mysql/blob/696fc899126ae00771b5d87bdadae836e704ae7d/8.0/docker-entrypoint.sh

mysql/docker-entrypoint.sh at 696fc899126ae00771b5d87bdadae836e704ae7d · docker-library/mysql · GitHub
そしてこのコードは次の条件から分岐している。

# docker-entrypoint.sh
if [ ! -d "$DATADIR/mysql" ]; then
....

# https://github.com/docker-library/mysql/blob/696fc899126ae00771b5d87bdadae836e704ae7d/8.0/docker-entrypoint.sh#L100


$DATADIRはとりあえずdatadirの値が来ているようで、設定上/var/lib/mysqlに対応していた。 https://github.com/docker-library/mysql/blob/fc3e856313423dc2d6a8d74cfd6b678582090fc7/8.0/config/my.cnf#L25

ので、上のdocker-compose.ymlでは/var/lib/mysqlmysql_dataディレクトリをマウントしているため、今回であれば初回起動時もしくはmysql_dataディレクトリが空の時、docker-entrypoint-initdb.d内の.sqlファイル等が呼ばれる気がする。