ファミコンエミュレータ作り その5
FC emulator作り with golang
完成までのマラソン記事。がんばってやり切るぞい。
また、注意点としてこの記事はコードを更新しながら作成していっていくので記事で書いてある内容と実際のコードに差が生まれる可能性はあります。
お問い合わせは私のツイッターまでお願いします。@jumdtw
ファミコンの画面を作る その二
ほんと時間かかった。これ完成するのか?
ファミコンの描画
とりあえず以下の画像を見てください
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という情報量です。
以下の画像を見てください
メモリから計128bitの情報を取り出すのですが、tileでの各pixelの情報はその128bitのビット列を画像のように配置することで正しく読みだすことができます。紛らわしい。しかし便利なことにyy-chrというソフトがあるのでこのようは形式の画像は簡単に作れる。
そんでもってこの形式どおり画面に出すためのものがhighbit, lowbitである。128bitを64~128bitと0~63の半分に分けることで順々に取り出すことができる。
実行結果
そんでもって今回描画したのが以下の画像である。
はい、かわいい。色はちょっと難ありだがすでに美少女である。
しかし、問題がある。以下は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のメモリへの書き込みをする機構を作っていく。
おまけ
適当に色つけてらたらサーモグラフィみたいになって笑った。
参考
YY-CHR @wiki 電子書籍「ファミコンゲーム製作入門」(おまけ付き) ギコ猫でもわかるファミコンプログラミング Memory-Map of NES ファミコンのグラフィックスの省メモリ化テクニックとは? Go言語 - 複数ファイルのコンパイル Golangで自分自身で定義したパッケージをインポートする方法あれこれ 配列と Slice Go言語でバイナリファイルを読み込む