プログラミングや低レイヤで遊ぶ人

基本的には遊んだことを記事にしていく

ファミコンエミュレータ作り その5

FC emulator作り with golang

完成までのマラソン記事。がんばってやり切るぞい。

また、注意点としてこの記事はコードを更新しながら作成していっていくので記事で書いてある内容と実際のコードに差が生まれる可能性はあります。

コード本体    

お問い合わせは私のツイッターまでお願いします。@jumdtw

ファミコンの画面を作る その二


ほんと時間かかった。これ完成するのか?

ファミコンの描画


とりあえず以下の画像を見てください
f:id:jumdtw:20200602232208j:plain
tile は描画の単位、block はパレット設定の単位と思っておいてください。
tile は8pixel x 8pixel、block は tile が2x2です(つまり、16pixel x 16pixel)。

画面全体は256pixel x 240pixelなので、tileで表すと 32 x 30 = 960 です。
下記はファミコンPPUを定義した構造体とメモリマップです(PPUはファミコンが持ってる描画専用の機構。CPUとは別に強そうな機構があるって思っておけばおけ)。

type ppu struct {
    // memory
    // 0x0000 ~ 0x0fff : rom #0 4096
    // 0x1000 ~ 0x1fff : rom  #1 4096
    // 0x2000 ~ 0x23bf : name table #0  960 = 32 * 30
    // 0x23c0 ~ 0x23ff : attribute #0   64 = 0xff - 0xc0
    // 0x2400 ~ 0x27bf : name table #1  960 = 32 * 30   .
    // 0x27c0 ~ 0x27ff : attribute #1   64 = 0xff - 0xc0
    //          .
    //  ミラーやら拡張機能やら
    //          .
    // 0x3f00 ~ 0x3f0f : BG palette  4 color * 4 palette 
    // 0x3f10 ~ 0x3f1f : sprite palette 4 color * 4 palette
    memory [memCap]uint8
}

ppuがファミコン描画の要なのでとりあえずcpu同様ppuの構造体を定義していきます。

正直現状必要なのがmemoryだけなのでmemory配列のみを持った構造体となります。

使うのは 0x2400 ~ 0x27bf : name table #1 960 = 32 * 30 です。
このメモリの部分に書き込むことによって画面に描画できます。(正確には設定用のレジスタにここに書くよ!ってことを書かなければならないのですがとりあえず今回は省きます。じゃないと完成しないからね♡)
実際のromデータは 0x1000 ~ 0x1fff : rom #1 4096 に、どのパレットを使っているのかの情報は 0x27c0 ~ 0x27ff : attribute #1 64 = 0xff - 0xc0 です。
つまりエミュレータをつくるときはこのメモリの部分を描画すればいいわけです。
前回のノイズ描画を応用して作っていきます。

タイルごとの描画


前回のコードでは描画単位は pixel 単位でしたがファミコンでは tile 単位なのでとりあえずその部分を作りました。

FC_PPU/emuPPU.go
func drawTile(g *Game,tileheadaddr int,numblock int,numtile int){
    var patternNum uint8
    var palletnum uint8
    //var palletnum uint8
    // chrnumにはname tableから、bgまたはspriteの番号を取り出す。
    chrnum := g.ppuemu.memory[0x2400 + numtile]
    // 番号からほしいタイルのメモリ番号の頭アドレス.ここから128bitとりだす。
    var romaddr uint64 = uint64(0x1000) + uint64(chrnum)*16
    var pp int

    highbit, lowbit := romdatareturn(g,romaddr)

    for i :=0; i<8; i++ {
        for k :=0; k < 8; k++ {
            // pp : 書き込もうとしているピクセルのrcの場所の配列数宇
            pp = tileheadaddr+k*4*pixlsize+i*256*pixlsize*4*pixlsize
            // N tile目のpattern number
            patternNum, highbit, lowbit = patternNumreturn(highbit,lowbit)
            // patternNumがどのpalletnumであるか。入りえる値は0~3.
            palletnum = palletnumreturn(g,numblock,numtile)
            var rc, gc, bc uint8 = rgbreturn(g,patternNum,palletnum)

            g.noiseImage.Pix[pp] = rc
            g.noiseImage.Pix[pp+1] = gc
            g.noiseImage.Pix[pp+2] = bc
            g.noiseImage.Pix[pp+3] = 0xff
            
        }
    }
}


patternNumはパレットの色情報。palletnummはどのパレットを使うかの情報です(この辺の情報は参考のギコ猫でもわかるファミコンプログラミングを参照していただけるとわかりやすいです)。
多分説明が必要なのはhighbit, lowbitです。

ファミコンの画像フォーマット


ファミコンは 1 pixel に 2 bitの情報量です。1 tile 8 x 8 pixelなので 1 tile 128 bitという情報量です。

以下の画像を見てください
f:id:jumdtw:20200602231906j:plain
メモリから計128bitの情報を取り出すのですが、tileでの各pixelの情報はその128bitのビット列を画像のように配置することで正しく読みだすことができます。紛らわしい。しかし便利なことにyy-chrというソフトがあるのでこのようは形式の画像は簡単に作れる。
そんでもってこの形式どおり画面に出すためのものがhighbit, lowbitである。128bitを64~128bitと0~63の半分に分けることで順々に取り出すことができる。

実行結果

そんでもって今回描画したのが以下の画像である。
f:id:jumdtw:20200602231909j:plain
はい、かわいい。色はちょっと難ありだがすでに美少女である。

しかし、問題がある。以下はppuの初期化のコードである。

func initppuemu(ppuemu *ppu){
    // カートリッジ0x1を初期化
    //ReadBinary(path string, readsize int,memory []uint8,baseaddr uint32)([]uint8) {
    bufmemory := FileOp.ReadBinary("filename",4096)
    for i:=0 ;i<0xfff; i++{
        ppuemu.memory[0x1000+i] = bufmemory[i]
    }
    // name table #1 を初期化
    var baseaddr int = 0x2400
    var romnum uint8 = 0
    for i:=0 ;i<256;i++{
        ppuemu.memory[baseaddr] = 0
        baseaddr++
    }

    for i:=0 ;i<16;i++{
        for k:=0;k<8;k++{
            ppuemu.memory[baseaddr] = 0
            baseaddr++
        }
        for k:=0;k<16;k++{
            ppuemu.memory[baseaddr] = romnum
            baseaddr++
            romnum++
            
        }
        for k:=0;k<8;k++{
            ppuemu.memory[baseaddr] = 0
            baseaddr++
        }
    }

    for i:=0 ;i<192;i++{
        ppuemu.memory[baseaddr] = 0
        baseaddr++
    }
    // attribute #1 を初期化
    for i:=0 ;i<64;i++{
        ppuemu.memory[0x27c0+i] = 0
    }
    // BG pallet を初期化
    ppuemu.memory[0x3f00] = 0x20
    ppuemu.memory[0x3f01] = 0x23
    ppuemu.memory[0x3f02] = 0x32
    ppuemu.memory[0x3f03] = 0x0f
    ppuemu.memory[0x3f04] = 0x01
    ppuemu.memory[0x3f05] = 0x16
    ppuemu.memory[0x3f06] = 0x26
    ppuemu.memory[0x3f07] = 0x2a
    ppuemu.memory[0x3f08] = 0x01
    ppuemu.memory[0x3f09] = 0x16
    ppuemu.memory[0x3f0a] = 0x26
    ppuemu.memory[0x3f0b] = 0x2a
    ppuemu.memory[0x3f0c] = 0x01
    ppuemu.memory[0x3f0d] = 0x16
    ppuemu.memory[0x3f0e] = 0x26
    ppuemu.memory[0x3f0f] = 0x2a
}


まぁ、正直代入されている値は問題ない。むしろあっている。問題はこれをプログラムで書き込んでいるということだ。今回作っているのはファミコンエミュレータなのでエミュレートしたCPUから書き込んでくれなくては困るのだ。

次回は前に作成したCPUから今回作成したPPUのメモリへの書き込みをする機構を作っていく。

おまけ


f:id:jumdtw:20200602231912j:plain
適当に色つけてらたらサーモグラフィみたいになって笑った。

参考 

YY-CHR @wiki 電子書籍「ファミコンゲーム製作入門」(おまけ付き) ギコ猫でもわかるファミコンプログラミング Memory-Map of NES ファミコンのグラフィックスの省メモリ化テクニックとは? Go言語 - 複数ファイルのコンパイル Golangで自分自身で定義したパッケージをインポートする方法あれこれ 配列と Slice Go言語でバイナリファイルを読み込む