2日目です. 前回はこれ
Golangでファミコンエミュを作る 1日目 - ぺんぎんさんのおうち
ちょっと雑談
ところでソースコードですが, 最終的にはGitHubにあげようと思ってます.
"n日で作るファミコンエミュレータ" なので30日OS本を倣って01_day, 02_day, ... みたいな感じで管理していこうかな〜とか考えたんですが, なにせ僕が何もわかっていなくて日によっては解説だけで終わっちゃいそうなんですよ. うーん..
いきなり完成してるコード見せるよりかは, 少しずつ足されていってる感じを見てもらった方が良さそうですね.
ということでリポジトリを作りました.
READMEとかまだちゃんと整備してないのでもう少しお待ちを.
(あ, あとこれ毎日投稿できるわけではないので)
ここから本題
さて前回はNESファイルのヘッダーを読み込んで各値を表示したところで終わりました. 念のため出力結果を再掲します.
Constant : 4E45531A
PRG ROM SIZE : 2
CHR ROM SIZE : 1
Flags6 : 00000001
Flags7 : 00000000
PRG RAM SIZE : 0
Flags9 : 00000000
Flags10 : 00000000
Flagsは今後のために二進8桁で表示しています.
フラグはひとまず置いておいて, この結果からわかるのは
プログラム部分が 2 * 16 KB = 32 KB
スプライト部分が 1 * 8 KB = 8 KB
ということです.
なんとなくreadNextBytesを使って32 KBと8 KBの読み込みをすればいいのかな〜と気づきますね.
ただ, それぞれのROMのサイズがわかっていても, 途中で別のデータが含まれている可能性があるのでまだ読み込みをするのは早いです. もし何か含まれていたらその分だけズレて読み込んでしまいますからね.
ではどうするかというと, NESファイルフォーマットを確認します.
前回はヘッダー部分だけを見ましたが, 今回はNESファイル全体のフォーマットに注目します.
- Header (16 bytes)
- Trainer, if present (0 or 512 bytes)
- PRG ROM data (16384 * x bytes)
- CHR ROM data, if present (8192 * y bytes)
- PlayChoice INST-ROM, if present (0 or 8192 bytes)
- PlayChoice PROM, if present (16 bytes Data, 16 bytes CounterOut)
こうなってました.
最初の16bytesがヘッダーなのは前回確認しました.
CHR ROM以降は無視するとしてPRG ROMの前に何かやっかいなのがいます.
Trainer というセクションが 0 or 512 bytes です. あるかもしれないし, ないかもしれない. (面倒ですね)
どこかに情報がないものかと探していると, Flags6に書いてありました.
Flags 6
76543210 | | | | | | | | | | | | | | | +- Mirroring: 0: horizontal (vertical arrangement) (CIRAM A10 = PPU A11) | | | | | | | 1: vertical (horizontal arrangement) (CIRAM A10 = PPU A10) | | | | | | +-- 1: Cartridge contains battery-backed PRG RAM ($6000-7FFF) or other persistent memory | | | | | +--- 1: 512-byte trainer at $7000-$71FF (stored before PRG data) | | | | +---- 1: Ignore mirroring control or above mirroring bit; instead provide four-screen VRAM ++++----- Lower nybble of mapper number
Flags6の第2ビットが1であるときにtrainerが512bytes含まれているようです.
$7000-$71FF の位置(PRGの前)っていうのが今の所よくわかりませんが, Flags6の値は00000001だったのでとりあえず考えなくて良さそうです(trainerセクションがないので).
ただ第0ビットが1なので "Mirroring" が vertical であることはメモしておきましょう.
ここまでで, NESファイルの構造を簡単に表してみます.
最初の16bytesがヘッダー, Trainerは含まれていないのでヘッダーの直後にPRG-ROMが32 KB, その後ろにCHR-ROMが8 KBです.
| Header [16 bytes] | PRG ROM [32 KB] | CHR ROM [8 KB] |
ではNESの読み込みをします.
読み込むバイト数はわかっているので, readNextBytesを使って
programROM := readNextBytes(f, int(header.PRG_ROM_SIZE) * 0x4000)
characterROM := readNextBytes(f, int(header.CHR_ROM_SIZE) * 0x2000)
それぞれのROMサイズは構造体でuint8として定義していたので, intにキャストしてあげないと怒られちゃいます.
uint8が 0 ~ 255 なので * 0x4000 は許されないっぽいですね. * 0x2000 も同様なのでキャストします.
読み込まれたバイト数を確認します.
fmt.Printf("Program ROM size : %x\n", len(programROM))
fmt.Printf("Chatacter ROM size : %x\n", len(characterROM))
### 実行結果 ###
Program ROM size : 8000
Chatacter ROM size : 2000
2 * 0x4000 = 0x8000
1 * 0x2000 = 0x2000
なのでちゃんと読み込みができています.
ちなみにCHR-ROMを読み込んだあとに1バイト読み込もうとすると「もう読み込めるものがないよ(EOF)~」とエラーが出ます.
やはり読み込んだからにはROMの中身を見てみたいですよね. 見ますか.
以下のコードを足して実行するとバイト列がぶわーっと出てきます.
fmt.Printf("Program ROM : %x\n", programROM)
fmt.Printf("Character ROM : %x\n", characterROM)
ぶわーと出てきても, それが本当にプログラムROMとキャラクターROMとして正しいのかってわかりませんよね. "ほんまにあっとるんか?" と疑問に思うわけです.
実はこのサンプルNESをダウンロードしたときに "character.chr" というファイルも付いてきていて, これはキャラクターROM部分だけを切り取ったものです.
見比べると最初に0が並んでいて, 途中から"1c 3e 3e 3e ..." となっているのでおそらく正しいです.
ここまででヘッダーとプログラムとキャラクターのバイト列の切り出しができました.
あとはプログラムROMを読んでバイナリのfetchとdecode, 画面の描画でHello Worldの表示ができそうな気がしますね.
言うは易し行うは難し
まだ先は長いはずです. 他にもやることはたくさんあります多分.
とりあえずサンプルNESを読み込んでPRG-ROMの表示をしたので, バイト列を眺めながら多分こんな実装が必要なんだろうな〜という想像をします.
今日はもう少し進めようと思っていましたが, 予想より帰宅が遅くなってしまい時間が取れなかったのでこのあたりで終わります.
この週末から来週にかけて予定が多めなので進捗は出せないと思うので隙間の時間でドキュメントとか読みます.
それでは.