ぺんぎんさんのおうち

日記です。たまに日記じゃないこともあります。

Golangでファミコンエミュを作る 1日目

1日目です. 前回はこれ Golangでファミコンエミュを作る 0日目 - ぺんぎんさんのおうち (投稿したのさっきですけど)

 

今回はHello Worldを動かすサンプルNESの読み込みをやっていきます.

 

※ ヘッダーとHeaderの表記揺れがあります. 特に違いはありませんがプログラムが絡んでくるような場合には"Header"を, そうでない場合には"ヘッダー"を使うようにしています(例外もありますが気にしないでください).

 

 

NESファイルはここでダウンロードできます.

NES研究室 - サンプル

 

とりあえずファイル読み込みをします. が, バイナリファイルの扱い方がわからずいきなり壁にぶつかってしまいました. 

NESファイルの先頭にはヘッダーがあるはずなので, ヘッダー分のバイト数を読み込んだ上で構造体か何かで保持しておきたいです. 

 

調べていると以下のような記事が見つかりました. 

www.jonathan-petitcolas.com

バイナリファイルを読んでバイト列から構造体へ変換する方法が記されていました. 

 

 

ヘッダーの解析

ヘッダーにはNESファイルのどの位置にプログラムがあってどの位置にキャラクターがあるのか.. といった情報が書いてあります. ヘッダーの解析をしましょう.

 

 

Header部分に関しては以下のページに詳しく記載されています. 

INES - Nesdev wiki

 

ひとまずHeader部だけを読んでみます.

 The format of the header is as follows:

  • 0-3: Constant $4E $45 $53 $1A ("NES" followed by MS-DOS end-of-file)
  • 4: Size of PRG ROM in 16 KB units
  • 5: Size of CHR ROM in 8 KB units (Value 0 means the board uses CHR RAM)
  • 6: Flags 6
  • 7: Flags 7
  • 8: Size of PRG RAM in 8 KB units (Value 0 infers 8 KB for compatibility; see PRG RAM circuit)
  • 9: Flags 9
  • 10: Flags 10 (unofficial)
  • 11-15: Zero filled

 

0 ~ 3 : 定数 4E 45 53 1A

4 : プログラムROMのサイズ (16 KB 単位)

5 : キャラクター(スプライト)ROMのサイズ(8 KB 単位)

6 : フラグ6

7 : フラグ7

8 : プログラムRAMのサイズ(8 KB 単位)

9 : フラグ9

10 : フラグ10

11 ~ 15 : 0 埋め

 

全体で16bytesですね. 

 

これをGolang側で構造体として次のように定義しておきます.

type Header struct { 
    Constant [4]byte
    PRG_ROM_SIZE uint8
    CHR_ROM_SIZE uint8
    Flags6 uint8
    Flags7 uint8
    PRG_RAM_SIZE uint8
    Flags9 uint8
    Flags10 uint8
    _ [5]byte
}

 

 

これでNESファイルの構造(Header)が簡単にわかるようになりました. 

最初の16bytesを読み込んでHeader構造体に保持してもらいましょう.

 

こうすればプログラムROMのサイズが知りたいときには header.PRG_ROM_SIZE, キャラクターROMのサイズが知りたいときには header.CHR_ROM_SIZE とすることができます. 

 

 

 ヘッダー読み込み用の関数を定義しました.

func readHeader(file *os.File) *Header {
    header := Header{}

    data := readNextBytes(file, int(unsafe.Sizeof(header)))
    buffer := bytes.NewBuffer(data)
    err := binary.Read(buffer, binary.BigEndian, &header)
    if err != nil {
       fmt.Println("binary.Read failed", err)
    }

    return &header
}

readNextBytes()というのが, バイナリを指定したバイト数読み込む関数です.

 

それと各値を確認するための関数も定義しました.

func showHeader(header *Header) {
    fmt.Printf("Constant : %x\n", header.Constant)
    fmt.Printf("PRG ROM SIZE : %x\n", header.PRG_ROM_SIZE)
    fmt.Printf("CHR ROM SIZE : %x\n", header.CHR_ROM_SIZE)
    fmt.Printf("Flags6 : %x\n", header.Flags6)
    fmt.Printf("Flags7 : %x\n", header.Flags7)
    fmt.Printf("PRG RAM SIZE : %x\n", header.PRG_RAM_SIZE)
    fmt.Printf("Flags9 : %x\n", header.Flags9)
    fmt.Printf("Flags10 : %x\n", header.Flags10)

}

 

最後にmain()の実行部分だけを示します.

func main() {
    argc := len(os.Args)
    if argc < 2 {
       fmt.Println("input file name.")
       return
    }

    filename := os.Args[1]
    f, err := os.Open(filename)
    if err != nil {
       fmt.Println("Err : ", err)
    return
    }
    defer f.Close()

    header := readHeader(f)
    showHeader(header)

 

     // 

}

 

実行してみましょう.

## 実行結果 ##

 

Constant : 4e45531a
PRG ROM SIZE : 2
CHR ROM SIZE : 1
Flags6 : 1
Flags7 : 0
PRG RAM SIZE : 0
Flags9 : 0
Flags10 : 0

ちゃんと読み込めてるっぽいですね. ヨシッ

 

次はNESのファイルフォーマットの話と, プログラム部分/スプライト部分の切り分け + なにか進捗があれば.. という予定です. 

 

明日は1限から昼まで講義なので早めに寝ます.