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

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

ファミコンエミュレータ作り その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言語でバイナリファイルを読み込む

猫語しかしゃべらなくなった先輩のためにCatLangageを作成した

catlangage compiler


コンパイラといっていますがmingw用のアセンブラをはくだけです。

機能はかなり少ないですが、猫語からアセンブラをはくことが可能です。

こちらがソースコードです。

疑問・不備等があった場合は私のツイッターでご連絡ください。@jumdtw

事の経緯


ツイッターでにゃんにゃんしか言わなくなってしまった方がいらっしゃります。
f:id:jumdtw:20200530033350j:plain
※本人の許可はとっております


彼に何があったのでしょうか。それを想像するには私は未熟すぎます。しかし、何とかしてあげたい。そこで猫語からアセンブリ言語をはきだすコンパイラ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

実装方法


以下のサイトがとても参考になりました。

低レイヤを知りたい人のためのCコンパイラ作成入門

私が説明しようとするとほとんど以下のサイトの反復説明になってしますので今回の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


こんな感じです。これで彼は「おはよう」・「疲れた」・「肉食べたい」を猫語のみで表現できるのです!!!
f:id:jumdtw:20200530033350j:plain

まとめ


まぁ、ネタなのでこんなもんでしょう。コンパイラの勉強に使用した以下のサイトはとても勉強になったのでおすすめです。
低レイヤを知りたい人のための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先生がいらっしゃるのだから。
というわけで検索したわけですよ。圧倒的安心感のままね。

f:id:jumdtw:20200526154751j:plain

なんもでねぇ なぜなのか

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>


このようなコードがあった場合、表示の結果は
f:id:jumdtw:20200526155406j:plain
となります。

そのほかにもjavascriptなどを使えばカスタム属性内のデータを取り出せたりと色々できるようなので結構便利なのかも???

参考

【HTML入門】HTMLの属性ってなに?属性の役割と書き方 カスタムデータ属性とは?

gopher君ってしってるか?

golangのFAQを見たら面白かったよって話
その2

golang blog ついでにgolang FAQ見ていた時の話

What's the origin of the gopher mascot?


gopherって誰やねんってなっていたのですがこいつです。名前初めて知った。
f:id:jumdtw:20200524205657p:plain
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


以下のがモデルシートらしい。

f:id:jumdtw:20200524205700j:plain
by %Renée French

f:id:jumdtw:20200524205709p:plain
by %Renée French
歯の本数は定まっていないらしい。個人的には二本派


f:id:jumdtw:20200524205706p:plain
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 による画面の実装


ファミコンの画面を作っていくわけですが、まずファミコンの画面についてです。
f:id:jumdtw:20200521031001j:plain ファミコンは横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の色と輝度を設定するためのようです。

このサンプルを実行すると以下のようになります。
f:id:jumdtw:20200521031400j:plain 画像では止まって見えますが、実際にはノイズ画面のように動いています。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
}


ウィンドウのサイズをファミコン画面のピクセル数に合わせ、ノイズのようにランダムに動かすのではなく固定値を代入しています。
実行すると以下のようになります。
f:id:jumdtw:20200521031008j:plain これをCPUのメモリと連携させることでファミコンの画面を作っていきます。まともなFPSもでていますしね。(裏での話をするとまともなFPSを出してくれるエンジンがこれくらいしかなかったです。今時ドットを描画してくれるものは珍しいようです。)

今回ではここまでにしますが、次回ではPPU構造体を作成してCPUのメモリとPPUのメモリを連携させていきたいと思います。

参考

Ebiten website tour hello world golang image

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

FC emulator作り with golang

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

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

コード本体         

実際にファミコンに描画してみる

今回の記事のために下の画像のようなデータを用意した。
f:id:jumdtw:20200510203838j:plain こちらのデータはyychrというソフトを使用して作成している。
yychr wiki

これを描画していきますが、一つ注意点としてVblank中書かなければならないということです。
ファミコンが画面に描画している時画面には描画されないタイミングあります。そのvblankの間に描画処理をしないと画面に映し出すことができません。

やり方の案として二つあり
1、$2002の7bit目(vblank中は1になる)を確認してから書き込む.
2、NMIの設定を使ってvblank中に割り込み処理をを入れ、書き込む。

どっちでもいいんですが、今回は2のやり方でやっていきます。

// 7bit目:vblank時NMI割り込み
// 3bit目:romのデータの置き場所の指定みたいなやつ
$2000 = 0b10001000


本当はキチンと描画のソースコード張るべきなんですが、その辺の情報はネットにかなり落ちているので今回は割愛します。
下記が実際に描画した画像です。エミュレータはfceuxを使用しています。
fceux
f:id:jumdtw:20200510203831j:plain

画面下に1がたくさん描画されているのは私が面倒臭がって適当にコード組んだためです。でもってこれをエミュレータに追加していくわけです。

どうやって描画するか

今回は下記モジュールを使って描画していきます。
azul3d

このモジュールのexsampleの中から使えそうなものを探した結果以下のものがありました。

go run .\azul3d_multiwindow\

f:id:jumdtw:20200510203835j:plain

これをいい感じにいい感じして画面を作っていきます。
ソースは次回の記事で説明します。