ファミコンエミュレータ作り その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言語でバイナリファイルを読み込む
猫語しかしゃべらなくなった先輩のためにCatLangageを作成した
catlangage compiler
コンパイラといっていますがmingw用のアセンブラをはくだけです。
機能はかなり少ないですが、猫語からアセンブラをはくことが可能です。
こちらがソースコードです。
疑問・不備等があった場合は私のツイッターでご連絡ください。@jumdtw
事の経緯
ツイッターでにゃんにゃんしか言わなくなってしまった方がいらっしゃります。
※本人の許可はとっております。
彼に何があったのでしょうか。それを想像するには私は未熟すぎます。しかし、何とかしてあげたい。そこで猫語からアセンブリ言語をはきだすコンパイラ、catcomを作成することにしました。
catcomができること
まぁ、要するにネタコンパイラなのでできることは相当限られてきます。
・加算減算を行いその結果を終了コードとしてはく(しかも、数字は0~7までしか使えません)
・特定の文字列を出力する
これだけです。
しかし、これだけあれば彼は特定の文字列のみですが会話ができ終了コードを吐き出すことができるのです!!!!
ちなみに特定の文字列とは以下です。
・I want meet.
・goodmorning.
・Im tired.
もっといっぱい実装はできますがネタなのでこんなものでしょう。
catlangage 仕様
そんでもって猫語からアセンブリ言語をはくといいましたが、「にゃー」だの「ゴロゴロ」だの日本語でやるのは少々面倒くさいので英語表記での猫語で処理をしていきます。以下はサイトを参考にして得たものです。
・meow:にゃー
・purr:ゴロゴロ
・roar:ガオー
これを数値と演算子などに当てはめていきます。
数値
meowを0、purrを1として表現している。
数値は "meow" から始まる
- 0:meowmeowmeowmeow
- 1:meowmeowmeowpurr
- 2:meowmeowpurrmeow
- 3:meowmeowpurrpurr
- 4:meowpurrmeowmeow
- 5:meowpurrmeowpurr
- 6:meowpurrpurrmeow
- 7:meowpurrpurrpurr
演算子と文字列表示
演算子は "roar" から始まります。
+:roarmeow
-:roarpurr
文字列表示は "purr" から始まります。
・I want meet. :purrmeowmeow
・goodmorning. :purrmeowpurr
・Im tired. :purrpurrpurr
実装方法
以下のサイトがとても参考になりました。
私が説明しようとするとほとんど以下のサイトの反復説明になってしますので今回のcatlangage特有部分のみ説明していこうと思います。
トークンナイザ
以下がトークンナイザでの判別になります。
// tokenize.cpp //空白文字をスキップ if(isspace(*p)||*p=='\n'||*p=='\t'){ p++; continue; } // ステーキの判別 if(strncmp(p,"purrmeowmeow",12)==0&&!is_alnum(p[12])){ token.ty = TK_MEET; token.str = p; p+=12; tokens.push_back(token); continue; } // . // . // こんな感じで文字列の判別をおこなう // . // . // +の判別 if(strncmp(p,"roarmeow",8)==0&&!is_alnum(p[8])){ token.ty = TK_PLUS; token.str = p; p+=8; tokens.push_back(token); continue; } // . // . // こんな感じで+-の判別をおこなう // . // . // 数値の判別 // 0 if(strncmp(p,"meowmeowmeowmeow",16)==0&&!is_alnum(p[16])){ token.ty = TK_NUM; token.val = 0; token.str = p; p+=16; tokens.push_back(token); continue; } // . // . // こんな感じで数値の判別をおこなう // . // .
このように一つ一つ判別していきます。
そのためあんまり多くの判別はできません(本来はこんなことをしないで文字か数字かで判断しますが、猫語のためにこんな判断をしています)。
パーサ
以下のようにパースしていきます。
expr = add add = primary ("+" primary | "-" primary)* | meet | tired | hello primary = num
それで実際にこれをコードで実装すると以下のようになります。
Node *primary(){ if(tokens[pos].ty == TK_NUM){ return new_node_num(tokens[pos++].val); } printf("%s\n",tokens[pos].str); printf("error gen node\n"); exit(1); } Node *add(){ if(tokens[pos].ty==TK_MEET){ return new_node(ND_MEET,NULL,NULL); }else if(tokens[pos].ty==TK_HELLO){ return new_node(ND_HELLO,NULL,NULL); }else if(tokens[pos].ty==TK_TIRED){ return new_node(ND_TIRED,NULL,NULL); } Node *node = primary(); for(;;){ if(tokens[pos].ty==TK_PLUS){ pos++; node = new_node(ND_PLUS,node,primary()); }else if(tokens[pos].ty==TK_MINUS){ pos++; node = new_node(ND_MINUS,node,primary()); }else{ return node; } } } Node *expr(){ return add(); }
expr始動で解析をしていきます。
最初に文字列の表示をするか否かを判別し、文字列でなかったら数字確定なので加算減算での解析をします。機能も少ない分コードもそこまで複雑ではないです。
コードジェネレーター
私の環境では以下の環境でアセンブラを処理します。
> gcc --version gcc (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0 Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. > as --version GNU assembler (GNU Binutils) 2.30 Copyright (C) 2018 Free Software Foundation, Inc. This program is free software; you may redistribute it under the terms of the GNU General Public License version 3 or later. This program has absolutely no warranty. This assembler was configured for a target of `x86_64-w64-mingw32'.
なのでそのためのコードを生成します。
以下はサンプルです。
// 文字列の表示 .text .LC0: .ascii "hello\0" .globl main .def main; .scl 2; .type 32; .endef .seh_proc main main: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $32, %rsp .seh_stackalloc 32 .seh_endprologue call __main leaq .LC0(%rip), %rcx call puts movl $0, %eax addq $32, %rsp popq %rbp ret .seh_endproc
// 加算 .text .def __main; .scl 2; .type 32; .endef .globl main .def main; .scl 2; .type 32; .endef .seh_proc main main: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $32, %rsp .seh_stackalloc 32 .seh_endprologue call __main push $1 // 数値を二つpushしておく push $4 pop %rdi // push した値をそれぞれレジスタに入れる pop %rax add %rdi, %rax // その値を使って加算する push %rax pop %rax // raxにpopして終了コードとして返す addq $32, %rsp popq %rbp ret .seh_endproc
これらコードを生成するためのコードジェネレータが以下のようになります。
// comcat.cpp void gen(Node *node){ if(node->ty == ND_MEET){ printf(" .text\n"); printf(".LC0:\n"); printf(" .ascii "); printf("\""); printf("I want meet.\\0"); printf("\""); printf("\n"); printf(" .globl main\n"); printf(" .def main; .scl 2; .type 32; .endef\n"); printf(" .seh_proc main\n"); printf("main:\n"); printf(" pushq %%rbp\n"); printf(" .seh_pushreg %%rbp\n"); printf(" movq %%rsp, %%rbp\n"); printf(" .seh_setframe %%rbp, 0\n"); printf(" subq $32, %%rsp\n"); printf(" .seh_stackalloc 32\n"); printf(" .seh_endprologue\n"); printf(" call __main\n"); printf(" leaq .LC0(%%rip), %%rcx\n"); printf(" call puts\n"); printf(" movl $0, %%eax\n"); printf(" addq $32, %%rsp\n"); printf(" popq %%rbp\n"); printf(" ret\n"); printf(" .seh_endproc\n"); return; } // . // . // ND_MEETなどだったらその段階で文字列表示確定なのでそのままサンプルのようなコードをはく // . // . // 数値は計算に必要なのでスタックにpushする if(node->ty == ND_NUM){ printf(" push $%%d\n",node->val); return; } // 数値・文字列でなかったらスタックの値を使って計算していく printf(" .text\n"); printf(" .def __main; .scl 2; .type 32; .endef\n"); printf(" .globl main\n"); printf(" .def main; .scl 2; .type 32; .endef\n"); printf(" .seh_proc main\n"); printf("main:\n"); printf(" pushq %%rbp\n"); printf(" .seh_pushreg %%rbp\n"); printf(" movq %%rsp, %%rbp\n"); printf(" .seh_setframe %%rbp, 0\n"); printf(" subq $32, %%rsp\n"); printf(" .seh_stackalloc 32\n"); printf(" .seh_endprologue\n"); printf(" call __main\n"); gen(node->lhs); gen(node->rhs); printf(" pop %%rdi\n"); printf(" pop %%rax\n"); switch(node->ty){ case ND_PLUS: printf(" add %%rdi, %%rax\n"); break; case ND_MINUS: printf(" sub %%rdi, %%rax\n"); break; } printf(" addq $32, %%rsp\n"); printf(" popq %%rbp\n"); printf(" ret\n"); printf(" .seh_endproc\n"); }
実行結果
文字列出力コードは以下です。
// test.cat purrmeowmeow
実行コードはこちらです。ちなみに、アセンブラをprintfで表示しているだけなのでパイプなどを使っていい感じにファイルに出力します。
// 文字列出力 > .\bin\comcat.exe test.cat > test.s > as -o test.o test.s > gcc -o test test.o > test.exe I want meet.
演算のコードは以下です。
// calc.cat meowmeowmeowpurr roarmeow meowmeowmeowpurr
実行コードはこちらです。windowsでは%errorlevel%で終了コードを確認できます。
> .\bin\comcat.exe calc.cat > calc.s > as -o calc.o calc.s > gcc -o calc calc.o > calc.exe > echo %errorlevel% 2
こんな感じです。これで彼は「おはよう」・「疲れた」・「肉食べたい」を猫語のみで表現できるのです!!!
まとめ
まぁ、ネタなのでこんなもんでしょう。コンパイラの勉強に使用した以下のサイトはとても勉強になったのでおすすめです。
低レイヤを知りたい人のためのCコンパイラ作成入門
参考
MinGWのアセンブラでhello world
低レイヤを知りたい人のためのCコンパイラ作成入門
猫の英語辞典:猫の鳴き声って英語でなんて言うの?
HTML5 カスタム属性のこと知らなかった件について
web作成のバイトしてるくせにweb初心者の男
バイト先でweb画面入力のバイトをしているが自分が無知であることが改めて実感できた。
それは突然の出会いだった
他のwebサイトの情報を引用しようと思いデバッグコンソールを開いたら以下のようなコードを見つけました。
<tr data-parts="36" data-unique="uniq000004" class=""> <th data-parts="36" data-unique="uniq000004" class="">hoge</th> <td data-parts="36" data-unique="uniq000004">fuga</td> </tr>
data-parts?????data-unique??????とは。知らない属性が飛んできた。
google先生~~~~
しかし、私は慌てなかった。なぜならみんな大好きgoogle先生がいらっしゃるのだから。
というわけで検索したわけですよ。圧倒的安心感のままね。
なんもでねぇ なぜなのか
10分くらい検索したのですが何も情報は得られませんでした。google先生に裏切られた気分。
そんでもって10分間検索してて思ったのは何もないってことは存在しないって疑惑もあるわけなので、検索の仕方を変えなきゃなって考えに行きつきました。
そこで共通部分の「data-」で検索したところ出てきました。
カスタム属性
どうやら「data-」とつけると自分の好きな属性を作成できるらしいです。HTML5から追加されたそうです。
属性とは?
その要素に設定をつけるものらしいです。例えば「style=」とかですね。これでスタイルの設定をつけることができます。
このカスタム属性ですが、CSSと関連付けることができたりするので
<style> [data-hoge="red"]{ color:red; } [data-hoge="blue"]{ color:blue; } </style> <html> <body> <p data-hoge="red">redredredred</p> <p data-hoge="blue">blueblueblue</p> </body> </html>
このようなコードがあった場合、表示の結果は
となります。
そのほかにもjavascriptなどを使えばカスタム属性内のデータを取り出せたりと色々できるようなので結構便利なのかも???
参考
gopher君ってしってるか?
golangのFAQを見たら面白かったよって話
その2
golang blog ついでにgolang FAQ見ていた時の話
What's the origin of the gopher mascot?
gopherって誰やねんってなっていたのですがこいつです。名前初めて知った。
by %Renée French
個人的にはかなり好き。かわいい。
以下はgolang FAQの引用です。
The mascot and logo were designed by Renée French, who also designed Glenda, the Plan 9 bunny. A blog post about the gopher explains how it was derived from one she used for a WFMU T-shirt design some years ago. The logo and mascot are covered by the Creative Commons Attribution 3.0 license. The gopher has a model sheet illustrating his characteristics and how to represent them correctly. The model sheet was first shown in a talk by Renée at Gophercon in 2016. He has unique features; he's the Go gopher, not just any old gopher.
Renée Frenchさんの著作権らしく、the Creative Commons Attribution 3.0 licenseなるものをもっているらしい。なにそれ?
the Creative Commons Attribution 3.0 license
以下はwikiの引用です。
クリエイティブ・コモンズ・ライセンス(英語: Creative Commons license、略称: CC license)とは、クリエイティブ・コモンズが定義する著作権のある著作物の配布を許可するパブリック・ライセンス(英語版)の一つである。
要は配布用のライセンスらしい。今回の場合、クレジット表記をすればよいっぽい。二次創作もいっぱいされている。かわいいけどこれの同人誌は別にいらないかな。
model sheet
以下のがモデルシートらしい。
by %Renée French
by %Renée French
歯の本数は定まっていないらしい。個人的には二本派
by %Renée French
ものを持つ方法は脇に挟むか手で触ることらしい。某たぬきやん
参考
golang FAQ
Go言語のマスコットキャラクター「Gopher」を愛でる
the Creative Commons license wiki
golangのFAQを見たら面白かったよって話 その1
golang FAQ
golang blog ついでにgolang FAQ見ていた時の話
Why is my trivial program such a large binary?
golangの公式にFAQがあったので眺めていたら以下の質問がありました。
Why is my trivial program such a large binary?
要はhello, worldみたいな小さいコードなのになんでビルドするとでかい実行ファイルになるのかということらしいです。
え?そなの?。。。。CPU使用率とかメモリ量とかしか見てなかったので知らんかった。
というわけで他言語と比較してみました。
・ windows10 home
・ gcc (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0
・ javac 12.0.1
・ go version go1.13.8 windows/amd64
以下は各言語のコードと実行したコマンドです。ただのhello, worldなので見なくても問題ないです。
//binary.c #include<stdio.h> int main(){ printf("hello, world\n"); return 0; }
//gcc exec cmd gcc -O3 binary.c
//Binary.java class Binary { public static void main(String[] args) { System.out.println("Hello, world."); } }
//javac exec cmd javac Binary.java
//binary.go package main import "fmt" func main(){ fmt.Print("hello, world\n") }
//go exec cmd go build binary.go
そんでもって生成されたバイナリのサイズが以下のものです。
gcc : 54,058 byte javac : 419 byte go : 2,161,115 byte
比較するとかなりデカかった。
なんでや...
なぜバイナリがでかくなるのか
FAQなのですぐ下に答えは書いてありました。以下はFAQの引用です。
The linker in the gc toolchain creates statically-linked binaries by default. All Go binaries therefore include the Go runtime, along with the run-time type information necessary to support dynamic type checks, reflection, and even panic-time stack traces. A simple C "hello, world" program compiled and linked statically using gcc on Linux is around 750 kB, including an implementation of printf. An equivalent Go program using fmt.Printf weighs a couple of megabytes, but that includes more powerful run-time support and type and debugging information. ・ ・ ・
つまり、デバッグ情報があるので重くなっているらしい。
-ldflags=-w のオプションをつけるとDWARFの生成がなくなり軽くなるらしい。とりあえずやってみました。-ldflags=-w のオプションをつけただけです。
go build -ldflags=-w .\binary.go
noflag.exe : 2,106,368 byte flag.exe : 1,653,248 byte
確かにだいぶ下がっていますね。
でも、DWAEFとは......?
DWAEFとは
どうやらデバッグ情報の形式らしい。
しかし、私の環境はwindowsなのでPDBという形式になるらしい。
以下のもコマンドでデバッグ環境を入れられるらしいです。
> go get -u github.com/derekparker/delve/cmd/dlv
私の環境では以下のようになりました。
> dlv debug binary.go Type 'help' for list of commands. (dlv) b main.main Breakpoint 1 set at 0x4bb156 for main.main() C:/Users/ttnmr/HOME/tmp/binary.go:5 (dlv) c > main.main() C:/Users/ttnmr/HOME/tmp/binary.go:5 (hits goroutine(1):1 total:1) (PC: 0x4bb156) 1: package main 2: 3: import "fmt" 4: => 5: func main(){ 6: fmt.Print("hello, world\n") 7: } (dlv)
gdbなどと使い方は同じのようです。これでfmt.printなどでデバッグしなくてもスマートにできそう。
参考
golang FAQ
デバッグ情報の歩き方
Microsoft、PDBフォーマットの情報をGitHubで公開
はじめてのgolang デバッグ&テストコード
ファミコンエミュレータ作り その4
FC emulator作り with golang
完成までのマラソン記事。がんばってやり切るぞい。
また、注意点としてこの記事はコードを更新しながら作成していっていくので記事で書いてある内容と実際のコードに差が生まれる可能性はあります。
反省の儀
これまでの記事があまりに雑すぎたのでここで懺悔します。
時間がかかってもちゃんとやった方がいいよね!
ファミコンの画面を作る その一
ebiten による画面の実装
ファミコンの画面を作っていくわけですが、まずファミコンの画面についてです。
ファミコンは横256×縦240のピクセルで構成されています。(実際にコードを組む時は縦横256ピクセルとして処理をしていくのですが、その理由はまた別のタイミングで)
今回はEbitenという2Dのゲームエンジンを使用して実装していきます。
Ebiten website
sampleにあるNoiseをいじって画面を作っていきます。
とその前に、tourにある基本部分のコードの解説だけしていきます。
興味のない方は飛ばしてしまってオッケーです。
tourには詳しい解説が乗っています。
ebiten 基本コード
以下がebitenの基本コードの全体です
package main import ( "log" "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" ) type Game struct{} func (g *Game) Update(screen *ebiten.Image) error { return nil } func (g *Game) Draw(screen *ebiten.Image) { ebitenutil.DebugPrint(screen, "Hello, World!") } func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 320, 240 } func main() { ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Hello, World!") if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) } }
上から一つづづ見ていきます。
type Game struct{}
golangではクラスなどはない変わりに構造体があります。
Gameの構造体を定義します。
ebiten の run.goには以下の定義があります。
// Game defines necessary functions for a game. type Game interface { // Update updates a game by one tick. The given argument represents a screen image. // // Basically Update updates the game logic. Whether Update also draws the screen or not depends on the // existence of Draw implementation. // // The Draw function's definition is: // // Draw(screen *Image) // // With Draw (the recommended way), Update updates only the game logic and Draw draws the screen. // In this case, the argumen . . .
このGame interfaceを使用するためにGame structを定義しているらしいです。
次は描画部分です。
func (g *Game) Update(screen *ebiten.Image) error { return nil } func (g *Game) Draw(screen *ebiten.Image) { ebitenutil.DebugPrint(screen, "Hello, World!") }
処理の順番的にはUpdate -> Drawのようです。
Updateでゲームロジックの更新。Drawで画面への描画をするらしいです。
頻度的には両方とも60fpsだそうです。
次で最後です。
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 320, 240 } func main() { ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Hello, World!") if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) } }
Layoutは今回のエミュレータでは使わないのであまり説明しませんが、公式いわくゲームの論理画面サイズを返す関数らしいです。
main関数ではwindowの生成とRunGame(&Game{})でゲームの開始をしています。
裏ではRunGame(&Game{})からrungameを呼び出していました。
rungameが再帰関数となっていてそこでゲームをループさせていたました。
これがebitenの基本コードです。
大した内容ではないのでgolangになれている人はすぐ理解できると思います。
Noise sample の編集
Noise sample の Update と Draw は以下の通りです。
func (g *Game) Update(screen *ebiten.Image) error { // Generate the noise with random RGB values. const l = screenWidth * screenHeight for i := 0; i < l; i++ { x := theRand.next() g.noiseImage.Pix[4*i] = uint8(x >> 24) g.noiseImage.Pix[4*i+1] = uint8(x >> 16) g.noiseImage.Pix[4*i+2] = uint8(x >> 8) g.noiseImage.Pix[4*i+3] = 0xff } return nil } func (g *Game) Draw(screen *ebiten.Image) { screen.ReplacePixels(g.noiseImage.Pix) ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.CurrentTPS())) } . . . func main() { ebiten.SetWindowSize(screenWidth*2, screenHeight*2) ebiten.SetWindowTitle("Noise (Ebiten Demo)") g := &Game{ noiseImage: image.NewRGBA(image.Rect(0, 0, screenWidth, screenHeight)), } if err := ebiten.RunGame(g); err != nil { log.Fatal(err) } }
golang の "image" モジュールをGame構造体に使用しています。
func NewRGBA(r Rectangle) *RGBA { return &RGBA{ Pix: make([]uint8, pixelBufferLength(4, r, "RGBA")), Stride: 4 * r.Dx(), Rect: r, } }
NewRGBAで新しいRGBA構造体を定義しています。UpdateではRGBAのPixを利用して画面への描画を行っているようです
。
pixelBufferLength(4, r, "RGBA")ではimage.Rect(0, 0, screenWidth, screenHeight)で生成したウィンドウの大きさを4倍した数を返しています。これは一ピクセルにRGBの色と輝度を設定するためのようです。
このサンプルを実行すると以下のようになります。
画像では止まって見えますが、実際にはノイズ画面のように動いています。CPU使用率もそんなに上がっていないですね。なんならchromeの方が高いです。
このサンプルを少し変えて行きます。
var rc, gc, bc uint8 = 1,0,0 const ( screenWidth = 256 screenHeight = 240 ) . . . . func (g *Game) Update(screen *ebiten.Image) error { // Generate the noise with random RGB values. const l = screenWidth * screenHeight for i := 0; i < l; i++ { //x := theRand.next() g.noiseImage.Pix[4*i] = rc g.noiseImage.Pix[4*i+1] = gc g.noiseImage.Pix[4*i+2] = bc g.noiseImage.Pix[4*i+3] = 0xff if rc==0xff{ rc = 0 gc = 0xff }else if gc ==0xff{ gc = 0 bc = 0xff }else{ bc =0 rc = 0xff } } return nil }
ウィンドウのサイズをファミコン画面のピクセル数に合わせ、ノイズのようにランダムに動かすのではなく固定値を代入しています。
実行すると以下のようになります。
これをCPUのメモリと連携させることでファミコンの画面を作っていきます。まともなFPSもでていますしね。(裏での話をするとまともなFPSを出してくれるエンジンがこれくらいしかなかったです。今時ドットを描画してくれるものは珍しいようです。)
今回ではここまでにしますが、次回ではPPU構造体を作成してCPUのメモリとPPUのメモリを連携させていきたいと思います。
参考
ファミコンエミュレータ作り その3
FC emulator作り with golang
完成までのマラソン記事。がんばってやり切るぞい。
また、注意点としてこの記事はコードを更新しながら作成していっていくので記事で書いてある内容と実際のコードに差が生まれる可能性はあります。
実際にファミコンに描画してみる
今回の記事のために下の画像のようなデータを用意した。
こちらのデータはyychrというソフトを使用して作成している。
yychr wiki
これを描画していきますが、一つ注意点としてVblank中書かなければならないということです。
ファミコンが画面に描画している時画面には描画されないタイミングあります。そのvblankの間に描画処理をしないと画面に映し出すことができません。
やり方の案として二つあり
1、$2002の7bit目(vblank中は1になる)を確認してから書き込む.
2、NMIの設定を使ってvblank中に割り込み処理をを入れ、書き込む。
どっちでもいいんですが、今回は2のやり方でやっていきます。
// 7bit目:vblank時NMI割り込み // 3bit目:romのデータの置き場所の指定みたいなやつ $2000 = 0b10001000
本当はキチンと描画のソースコード張るべきなんですが、その辺の情報はネットにかなり落ちているので今回は割愛します。
下記が実際に描画した画像です。エミュレータはfceuxを使用しています。
fceux
画面下に1がたくさん描画されているのは私が面倒臭がって適当にコード組んだためです。でもってこれをエミュレータに追加していくわけです。
どうやって描画するか
今回は下記モジュールを使って描画していきます。
azul3d
このモジュールのexsampleの中から使えそうなものを探した結果以下のものがありました。
go run .\azul3d_multiwindow\
これをいい感じにいい感じして画面を作っていきます。
ソースは次回の記事で説明します。