Posts Tagged with "Vivado HLx"

既に発行済みのブログであっても適宜修正・追加することがあります。
We may make changes and additions to blogs already published.

BSVの設計トライアル (11)

posted by sakurai on April 21, 2020 #244

BSVコード

nextStep()関数は以下のとおりです。

    function nextStep;
       case (state.step)
          S0: nextStep = S1;
          S1: nextStep = S2;
          S2: nextStep = S3;
          S3: nextStep = S4;
       endcase
    endfunction: nextStep

以下に4段のスタックとスタックポインタの実装(インスタンシエーション)を示します。

    // return stack
    RegFile#(UInt#(2), State_t) rs <- mkRegFile(0, 3);
    // stack pointer
    Reg#(UInt#(2)) sp <- mkReg(0);

検証用FSMの設計

これらのステート遷移の検証用FSMを設計します。全部でL1~L4の4レベルのコールネスティング関係を持ち、各ステートでは、主にステート変数の表示を行います。図244.1~図244.4に、レベル1からレベル4のステート遷移図を図示します。

図%%.1
図244.1 L1ステート遷移

図%%.2
図244.2 L2ステート遷移

図%%.3
図244.3 L3ステート遷移

図%%.4
図244.4 L4ステート遷移

検証結果の作成

expectedというファイル名で実行結果を作成しておきます。レベルとステートをサイクル毎に表示するFSMの動きを示します。

    L1 S0
    L1 S1
      L2 S0
      L2 S1
      L2 S2
        L3 S0
        L3 S1
          L4 S0
          L4 S1
        L3 S2
      L2 S3
      L2 S4
    L1 S2
    L1 S2

左矢前のブログ 次のブログ右矢

BSVの設計トライアル (10)

posted by sakurai on April 20, 2020 #243

スタック操作のBSV記述

共通シーケンスをコールする場合、これまで見てきたように、スタック操作をする必要がありますが、煩雑なのでこれを隠蔽することを考えます。と言ってもソフトウェアでやるように、push, pop, call, return等の概念を使います。また、これまでハードウェアという意識から「共通シーケンス」と称してきた概念を、ソフトウエアに合わせてサブルーチンと呼びます。

対象となる検証用FSMのステート定義を示します。例として4レベルのFSMを考えます。state変数は構造体で定義しています。

typedef enum { L1, L2, L3, L4 } Function_t deriving(Bits,Eq);
typedef enum { S0, S1, S2, S3, S4 } Step_t deriving(Bits,Eq);
    
typedef struct {
     Function_t func;
     Step_t step;
} State_t deriving(Bits,Eq);

rsはリターンスタック配列で、BSVでの実装例として、今回はRegFileで構成します。spはスタックポインタで配列インデックスです。Verilogにおける配列のdata変数への読み出し

    data <= rs[sp]

は、この実装では、

    data <=rs.sub(sp)

となり、逆にVerilogにおける配列へのdataの書き込み

    rs[sp] <= data

は、この実装では

    rs.upd(sp, data)

となります。

マクロ命令定義

これらを用いて作成したマクロ命令の定義文のコードです。

`define pushRet           rs.upd(sp, ret); sp <= sp + 1
`define popRet            state <= rs.sub(sp-1); sp <= sp - 1
`define call(SUB)         `_saveNext; state <= State_t {func:SUB, step:S0}
`define _saveNext         ret <= State_t {func:state.func, step:nextStep()}
`define return            state <= ret
`define next              state.step <= nextStep()

その説明を表243.1に示します。

表243.1 マクロ命令の定義
マクロ命令名 説明
pushRet 非リーフルーチン中で、他のルーチンを呼ぶ際に破壊されるretをスタックにプッシュするマクロ命令で、必ず先頭でpushするものとします。
popRet 非リーフ内で呼び出し元に戻るためのマクロ命令で、retを回復せずに、スタック中の戻りステートに直接戻るマクロ命令です。
call(SUB) サブルーチンコールです。次のステートをretに入れ、コール先にジャンプします。
_saveNext callに使用されており、次のステートをretに入れる内部マクロ命令です。後述のnextStep()関数を使用しています。ユーザが陽に使う必要は無いため、アンダースコアを付けています。
return リーフ内で呼び出し元に戻るためのマクロ命令です。
next 次のステートに進めるためのマクロ命令です。後述のnextStep()関数を使用しています。


左矢前のブログ 次のブログ右矢

BSVの設計トライアル (9)

posted by sakurai on April 17, 2020 #242

Direct Return

ついでに前稿で検討したダイレクトリターンがどうなるかを見ておきます。まず図で書けば、前稿のCaller Savedでは図242.1のようになります。左が一般的な、非リーフからのリーフのコール、右がダイレクトリターンです。コール先がリーフであると分かっている時は、リターンレジスタに戻り先を入れる代わりに、スタックをポップして戻り先をリターンレジスタに入れてからコールします。

図%%.1
図242.1 Caller Savedダイレクトリターン
一方、Callee Saved方式では、図242.2のようになります。同じく、左が一般的な、非リーフからのリーフのコール、右がダイレクトリターンです。
図%%.2
図242.2 Calee Savedダイレクトリターン
同じくコール先がリーフであると分かっている場合には、リターンレジスタに戻り先を入れる代わりに、そのままジャンプします。コール先を見ないという利点は無いものの、処理が単純化されます。

左矢前のブログ 次のブログ右矢

BSVの設計トライアル (8)

posted by sakurai on April 16, 2020 #241

Caller Saved

今回GameFSMを開発するにあたり、過去記事で開発したコール方式を踏襲しますが、今回はCaller Saved方式に変えてみたいと思います。これを図示すると、呼び出し側では、必ずリーフと思ってリターンレジスタにネクストステートを格納して共通シーケンスをコールします。BSVコードで書けば、コール先をtarget_state、戻り先ステートをnext_stateとして、

    return <= next_state;
    state <= target_state;

となります。

リーフシーケンス

呼ばれた側は、さらにサブ共通シーケンスをコールするかどうかは分かっているので、さらに呼ばない場合、つまりリーフシーケンスである場合は何もせずに

    state <= return;

を実行して戻ります。

非リーフシーケンス

他方、さらにサブ共通シーケンスの場合は、呼ばれた側ではまずリターンレジスタをプッシュします。

    return_stack[sp] <= return;
    sp <= sp + 1;

呼び出し元と同様に、サブ共通シーケンスをコールします。

    return <= next_state2;
    state <= sub_sequence;

戻る場合は、本来はリターンレジスタを回復してからそれをステートに入れるのですが、処理を簡略化して、

    state <= return_stack[sp - 1];
    sp <= sp - 1;

として戻ります。

最後に、前稿で実施したCaller Savedと今回実施予定のCallee Savedの方式の違いを図でまとめます。Caller Savedの利点は前述のとおり、呼ぶ側では同じシーケンスで良いことです。さらにreturnレジスタを壊すか壊さないかは自分の情報なので、判断し易いことが挙げられます。

図%%.1
図241.1 Caller SavedとCalee Savedの処理の違い

左矢前のブログ 次のブログ右矢

BSVの設計トライアル (7)

posted by sakurai on April 15, 2020 #240

mkConnection

BSVにはmkConnectionという機能があり、インターフェースの接続を効率的に行えます。

図%%.1
図240.1 Vivado sound階層ブロック図

図240.1にmkConnectionによる信号の接続及び、BSVで言うメソッドとVerilogで言う信号端子の関係を示します。

Verilogで表すと、モジュールefsmの端子out_code[4:0]から出力した信号code[4:0]が、モジュールsfsmの端子in_code[4:0]に接続されるものとします。この接続を実現するのは上位でのmkConnectionメソッドです。

まず、出力側モジュールefsmの読み出しメソッドは、interfaceブロックにおいてout_code()メソッドで記述されます。メソッドのリターンタイプはCode_tです。BSVのメソッド名がそのままVerilogの端子名になります。

    interface EFSM_ifc;
        method Code_t out_code();

上位での接続のための識別子は、"モジュール名"+"."+"BSVメソッド名(=Verilog端子名)"となります。EFSMモジュールのインスタンス名をefsmとすると、以下のようになります。

    efsm.out_code

一方、入力側モジュールsfsmへの書き込みメソッドには副作用があるため、interfaceブロックにおいて、method Actionとします。書き込みとVerilog端子は少々複雑で、Verilog端子名は"BSVメソッド名_入力パラメータ"となります。

    interface SFSM_ifc;
        method Action in(Code_t code);

上位での接続のための識別子は出力側と同じく、"モジュール名"+"."+"BSVメソッド名"となります。Verilog端子名とは若干異なります。

    sfsm.in

ここでは分かりやすさのため、メソッド名をinとしましたが、重なることはできないため、現実的にはinXXXXのように、意味のあるメソッド名を付けることになります。最後に上位での接続を示します。

    import Connectable::*;

    mkConnection(efsm.out_code, sfsm.in);

inとoutのどちらを先に書いても文法的には問題ありませんが、回路図の記法の通例として左から右に信号の流れを書くため、左側に出力メソッド、右側に入力メソッドを書くルールとします。

Verilog端子名の制御

注意:端子名制御はVerilogのみに影響を与えるため、mkConnection等のBSVの世界では影響を与えません。

defaultのVerilog端子生成ルールはコントロールすることができ、

  • 出力側の端子名をOUTPORTとしたい場合は(* result="OUTPORT" *)とインタフェースに記述します。
  • 入力側の端子名をINPORTとしたい場合は(* prefix="" *)としてメソッド名を消去し、さらに(* port="INPORT" *)とインタフェースに記述します。

メソッド名は大文字で始まることが許されていないため、任意のポート名を付けるにはこの制御指示子を用います。例えば、出力インタフェース部に

    method Bool fifo_ren();

という出力インタフェースがある場合、

    (* result="FIFO_REN" *)
    method Bool fifo_ren();              // output terminal "fifo_ren" -> "FIFO_REN"

という指示を与えることで、生成されるverilogにはFIFO_RENという端子が生成されます。

    // value method fifo_ren
    output FIFO_REN;

また、入力インタフェース

    method Action rom(Data_t indata);

においては、メソッド名がromでアーギュメントがindataであるため、defaultでは端子名はrom_indataとなります。これに対して、

    (* prefix="" *)
    method Action rom((* port="INDATA" *) // input terminal "rom_indata" -> "INDATA"
       Data_t indata);

という指示を与えることで、生成されるverilogにはINDATAという端子が生成されます。

    // action method rom
    input  [7 : 0] INDATA;

最後にmethod名を生かしたい場合には次のようにします。例えば

    method ActionValue#(Bit#(8)) read(); // パラレル出力メソッド
    method Action write(Bit#(8) nodata); // パラレル入力メソッド

のようにread及びwriteというパラレル入出力が有った場合、readという出力インタフェースに対して生成されたverilogは以下のようにmethod名がそのまま端子名になります。

    // read                           O     8 reg
    // RDY_read                       O     1
    // EN_read                        I     1

反対にwriteという入力インタフェースに対してはmethod名を消去しなければmethod名と変数名の結合となります。または、上記のようにmethod名を消去すると入力変数名が生きることになります。

そこで、method名を生かすには一度method名を消去して再度method名をポートで定義することで対処します。

    (* prefix="" *) // delete write
    method Action write((* port="write" *) Bit#(8) nodata); // パラレル入力メソッド

これにより生成されたverilogのコメント部は以下のとおりです。

    // write                          I     8
    // RDY_write                      O     1
    // EN_write                       I     1

左矢前のブログ 次のブログ右矢

BSVの設計トライアル (6)

posted by sakurai on April 14, 2020 #239

サウンドFSMの完成

ミキサーは4chある音源のミキシングを行います。ミキシングは符号無し8bit整数を16bitに符号拡張し、加算することで行います。サウンド平均化を試しましたが、かえって違和感が出るため、シンプルな加算としました。念のためにクリッピング処理を入れています。

完成したsound階層を図239.1に示します。

図%%.1
図239.1 Vivado sound階層ブロック図

従来回路図234.2と比べてサウンドチャネルが4倍になっただけでなく、ミキサー回路を新設したため、かなり大きくなっています。表239.1に各モジュールの物量(LUT数)を示します。LUTは使用個数は4,216個であり、全容量70,560個中の5.98%でした。またBRAMは使用個数は39.5であり、全容量216中の18.3%でした。

ゲーム全体の各モジュールの物量を表239.1に示します。色付け部分が今回BSVで開発したモジュールです。

表239.1 各モジュールの物量
モジュール 説明 物量 BSV行数 Verilog行数
LUT数 LUT割合[%] BRAM数
invader_move 自機、インベーダ、UFO、自弾、敵弾、スコア等を処理するFSM 3,239 76.7 0.0 2,125
mkSoundFSM3 UFO音を演奏するFSM 197 4.7 0.0 448 939
mkSoundFSM2 インベーダ歩行音を演奏するFSM 192 4.6 0.0 950
mkSoundFSM1 インベーダ爆発音を演奏するFSM 190 4.4 0.0 922
mkSoundFSM0 自弾発射音、自機爆発音、自機増加音を演奏するFSM 184 4.7 0.0 957
mixer 4チャネルのサウンドミキサー 44 1.0 0.0 30
graphic_control VGA映像信号生成回路 38 0.9 0.0 92
para_seri サウンドパラシリ変換、タイミング生成回路 22 0.5 0.0 101
sound_rom0 自弾発射音、自機爆発音、自機増加音を格納するROM 20 0.5 7.0 IP
sound_rom1 インベーダ爆発音を格納するROM 20 0.5 7.0 IP
sound_rom2 インベーダ歩行音を格納するROM 20 0.5 7.0 IP
sound_rom3 UFO音を格納するROM 20 0.5 7.0 IP
pattern_rom 自機、インベーダ、UFO、自弾、敵弾、スコア等の図形を格納するROM 7 0.2 3.5 IP
buttons スイッチとボタンをORする回路 4 0.1 0.0 21
display_out ブランキング期間に表示を抑止する回路 3 0.1 0.0 21
vram ビデオRAM 2 0.0 8.0 IP
onestage ゲームFSMとサウンドFSMの間のバッファ 2 0.0 0.0 43
clk_wiz クロック制御回路 1 0.0 0.0 IP

図239.2に全体のセルの配置図を示します。

図%%.2
図239.2 Vivadoセル配置図

例外動作

基本的に、サウンドエンジンはサウンド番号を受け、ROMに示されるサウンド長だけサウンドを演奏し、サウンド中に割込みが入る場合にはその割込みサウンドを演奏しますが、例外が2つあります。

  • UFO飛行音 --- UFO飛行音ONでサウンドを演奏し、終了してもUFO飛行音OFFが来るまでは演奏し続ける。
  • 自機増加音 --- 自機増加音演奏中に割込みが入ると聞こえなくなる場合がある。このため、自機増加音は優先サウンドとして扱い、他の割込みを受け付けない。

自機増加音は自弾発射音、自機爆発音と独立であるため、別チャネルとするほうが良いのですが、頻度が低いのと別チャネルにするとハードウエアが無駄のため、このような仕様としました。実験の結果、優先サウンドとしてもゲーム上特に問題はありませんでした。


左矢前のブログ 次のブログ右矢

BSVの設計トライアル (5)

posted by sakurai on April 13, 2020 #238

サウンドFSMの作成(3)

FORMAT機能、DATA機能からコールされるREADCOUNT共通シーケンス及び、そこからコールされるREADMEM共通シーケンスを解説します。

       //  input:  romaddr
       //  output: (romaddr,...,romaddr+3) => dcount;
       //          romaddr + 4 => romaddr;
       //
       rule ruleREADCOUNT (state.func == READCOUNT);

          rule ruleS0 (state.step == S0);
             state.step <= S1;
             ret2 <= ret; // push to stack
             i <= 3;
             workd <= 0;
          endrule

          rule ruleS1 (state.step == S1);
             state <= State_t {cat:state.cat, func:READMEM, step:S0};
             ret <= State_t {cat:state.cat, func:READCOUNT, step:S2};
             worka <= romaddr + i;
          endrule

          rule ruleS2 (state.step == S2);
             if (i == 0) begin
                state <= ret2;
                dcount <= workd<<8 | extend(romdata);
                romaddr <= romaddr + 4;
             end else begin
                state.step <= S1;
                workd <= workd<<8 | extend(romdata);
                i <= i - 1;
             end
          endrule

      endrule // READCOUNT

図示すると、図238.1のようなステート遷移となり、リトルエンディアンで格納されている4バイトの数値を1バイトずつ4回取り出し、dcountにまとめる機能を持ちます。

図%%.1
図238.1 READCOUNT共通シーケンスステート遷移図

次にREADMEMはROMから1バイト読み出す共通シーケンスです。当初はwireを用いても階層の上り下りでレイテンシがかかり、7サイクルとなりましたが、後述のmkConnectionを用いたことにより、RTLと同様の3サイクルの設計とすることができました。

上記のREADCOUNTから呼ばれる際はサイクル数は無関係ですが、サウンド再生中は正しいスループットで読み出す必要があるため、過去記事にもあるように、コール元が1サイクルとコール先(READMEM)が3サイクルの4サイクルで1バイトを読み出す前提で、FSMのクロック周波数=176.4KHzを決めています。

       // READ MEM
       //  input:  worka
       //  output: romdata;
       //
       rule ruleREADMEM (state.func == READMEM);

          rule ruleS0 (state.step == S0);
             addr <= worka;
             state.step <= S1;
          endrule

          rule ruleS1 (state.step == S1); // ROM address phase
             state.step <= S2;
          endrule

          rule ruleS2 (state.step == S2); // ROM data phase
             data <= romdata;
             state <= ret;
          endrule

        endrule // READMEM
 

左矢前のブログ 次のブログ右矢

BSVの設計トライアル (4)

posted by sakurai on April 10, 2020 #237

サウンドFSMの作成(2)

階層化ステートマシンを解説します。基本的にBluespecにおいてはrule文のガードによりステートに入る条件を定義し、rule文の内部でステートの処理を定義します。そこで、ruleを構造化することで、ステートの構造化を実現します。具体的には以下のような構造となります。

    rule ruleSOUND (state.cat == SOUND);

       rule ruleFORMAT (state.func == FORMAT);

          rule ruleS0 (state.step == S0); 
             // call readcount();
             state <= State_t {cat:SOUND, func:READCOUNT, step:S0}; 
             ret <= State_t {cat:SOUND, func:FORMAT, step:S1};
             case (code)
                'h1: romaddr <=     0 + 16;
                'h2: romaddr <=  4318 + 16;
                'h9: romaddr <= 13094 + 16;
             endcase
          endrule

          rule ruleS1 (state.step == S1);
             state <= State_t {cat:SOUND, func:DATA, step:S0};
             romaddr <= romaddr + extend(dcount);
          endrule

       endrule // FORMAT

ruleS0ではcodeによりROMアドレス先頭を決定します。wave formatの16バイト目からフォーマット長取得を行います。readcount()をコールしており、そのリターンはruleS1ステートです。readcount()ではdcount変数に4バイトの数値が得られます。

図%%.1
図237.1 FORMAT機能ステート遷移図
dcountにフォーマット長が得られたので、次にデータ長を取得します。
     rule ruleDATA (state.func == DATA);

         rule ruleS0 (state.step == S0);
             // call readcount();
             state <= State_t {cat:SOUND, func:READCOUNT, step:S0}; 
             ret <= State_t {cat:SOUND, func:DATA, step:S1};
             romaddr <= romaddr + 4; // skip "data"
         endrule

         rule ruleS1 (state.step == S1);
             state <= State_t {cat:SOUND, func:PLAY, step:S0}; 
             romaddr <= romaddr - 1;
         endrule

      endrule // DATA

dcountにデータ長が得られたので、実際のサウンドデータを出力するため、PLAYに移行します。

図%%.2
図237.2 DATA機能ステート遷移図
PLAYにおいては4倍のインターポレーションを行うため、4回同じデータを出力します。

左矢前のブログ 次のブログ右矢

BSVの設計トライアル (3)

posted by sakurai on April 9, 2020 #236

サウンドFSMの作成

waveフォーマット解析とデータ供給を実行する、メインのFSMの設計を行います。このような小さいFSMであっても階層設計を行います。その理由はソフトウェアと同じで、構造化のためです。最上位ステートは図236.1のようにINIT, SOUNDの2ステートとします。基本的にINITでは外部からサウンドコードが送信されてくるのを待ち、自分のコードであればSOUNDに遷移します。サウンド出力が終了すると、またINITに戻ります。

図%%.1
図236.1 最上位ステート

フラットなFSMではステート変数を整数で持ちますが、階層FSMでは構造体で持つことにします。階層は3階層で、上位2階層が意味のある機能を示し、最下層はステップとします。次に構造体の記述を示します。

    typedef struct {
           Category_t cat;
           Function_t func;
           Step_t step;
    } State_t deriving(Bits,Eq);

上位からカテゴリー、ファンクション、ステップと名付けました。最上位のカテゴリーは先の図のように、 INIT, SOUNDの2ステートからなります。

    typedef enum { INIT, SOUND } Category_t deriving(Bits,Eq);

INITでは中間階層である機能は特になく、2ステートを持ち、LRCLKと同期させます。これはFSMクロックがLRCLKよりも速いため、LRCLKと確率的に位相が合わなくなるためです。

SOUNDでは機能としてFORMAT, DATA, PLAY, READCOUNT, READMEMの5種を持ちます。それぞれの機能は、フォーマット解析、データ長取得、演奏です。さらにそこから呼ばれる共通シーケンス(サブルーチン)として、データ長読み出し、データ1バイト読み出しが存在します。

図%%.2
図236.2 SOUND階層内ステート
第2階層であるファンクション変数の定義です。
    typedef enum { FORMAT, DATA, PLAY, READCOUNT, READMEM } Function_t deriving(Bits,Eq);

同階層内にコール関係があり、DATAからREADCOUNTをコールします。さらにREADCOUNTからREADMEMをコールするので、リターンスタックは2段必要です。

最下層のステップの名前には特に意味が無く、シーケンシャルにステートを進めるだけです。シーケンシャルになる理由は、フォーマット解析やROMのデータ待ちなど様々です。

    typedef enum { S0, S1, S2, S3, S4, S5, S6 } Step_t deriving(Bits,Eq);

以上は宣言であり、実際のインスタンシエーションは以下のようにモジュール内部で行います。

    Reg#(State_t) state <- mkReg(State_t{cat:INIT,func:?,step:S0}),
         ret <- mkRegU,
         ret2 <- mkRegU;

ステート構造体stateと、さらに同じ構造を持つ2本のリターンレジスタret, ret2をインスタンスしています。


左矢前のブログ 次のブログ右矢

BSVの設計トライアル (2)

posted by sakurai on April 8, 2020 #235

ROMの作成

図234.2に示したように、サウンドROMが存在するので、BSVで作成します。ROMは合成には使用しませんが、bsimシミュレーションのために、 ROMは書き込まないRAMとして実装します。RAMはBSVではRegFileライブラリを使用します。RegFileは実際には1W5RのマルチポートRAMですが、他のポートは使用しません。

    import RegFile::*;

最初にこの行によりライブラリRegFileを導入します。インスタンスは次の行で行います。同時にROMファイルの内容をロードします。

    typedef Bit#(15) Addr_t;
    typedef Bit#(8) Data_t;
    RegFile#(Addr_t, Data_t) rom <- mkRegFileLoad("sound_data/ch00.hex", 0, 18593);

以下にROMファイルの内容を示します。チャネル0のファイルは3つのサウンドを持つ、18,594バイトのファイルです。

    52
    49
    46
    46
    d6
    10
    :

サウンドの難しさ

映像とサウンドを一致させるには難しさがあります。

  • ワンショット --- 映像のほうがサウンドよりも長いため、ひとつの映像事象において複数のサウンドが鳴ってしまう。例えば自弾がどこにも当たらずに飛行して壁で爆発するまでに、発射音が複数回鳴ってしまう。
    図%%.1
    図235.1 ワンショットNG
  • プリエンプション --- 映像よりもサウンドのほうが長いため、2度目の映像事象にサウンドが鳴らない。例えばインベーダが非常に短い間隔で消滅した場合、2度目の消滅音が無視されてしまう。
    図%%.2
    図235.2 プリエンプションNG
    これらは一見矛盾するため、解決法を考える必要があります。状態ではなくエッジでサウンドを起動すれば解決できそうです。ただし、サウンド再生中に中断及び再実行可能にする必要があります。
    図%%.3
    図235.3 ワンショット
    図%%.4
    図235.4 プリエンプション
    さらに、ゲーム制御とサウンドのFSMでは動作周波数が30倍も異なり、ゲーム制御FSMからのサウンド指示間隔が短いと取りこぼす可能性があります。しかしながら、実験の結果取りこぼしを対策するためのサウンドキューよりも、リアルタイム性を優先したほうが良いことが分かりました。従って、当初FIFOジェネレータで作成したサウンドキューを廃止して1段のバッファとします。
    図%%.5

図235.5 ゲーム制御FSMとサウンドFSMの相互作用

左矢前のブログ 次のブログ右矢


ページ: