Posts Tagged with "Design"

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

GameFSMの改良 (23)

posted by sakurai on December 11, 2025 #1048

対応するコードを示します。まず、前景は各色1bitカラー$(g, r, b)$で元と変わりません。

   Bit#(1) fg_g1 = !in_exp ? (in_data[2] & pack(fg_dt)) : 1'b0;
   Bit#(1) fg_r1 = !in_exp ? (in_data[1] & pack(fg_dt))
                           : ((in_data[2] | in_data[1] | in_data[0]) & pack(fg_dt));
   Bit#(1) fg_b1 = !in_exp ? (in_data[0] & pack(fg_dt)) : 1'b0;

これに4bitの背景を重ね合わせます。背景は$(g_3, g_2, g_1, g_0, r_3, r_2, r_1, r_0, b_3, b_2, b_1, b_0)$の各色4bitカラーです。ただし前稿のとおり、$g_3=r_3=b_3=0$であることからデータは省略でき、3bitずつROMに格納します。

   // 背景 GRB333(9bit)
   Bit#(3) bg_g3 = bg_data[8:6];
   Bit#(3) bg_r3 = bg_data[5:3];
   Bit#(3) bg_b3 = bg_data[2:0];

そこからスキャンのタイミングでこのように各色3bitずつ9bitを取り出します。

   // 3bit → 4bit (MSB=0 を付加)
   Bit#(4) bg_g4 = { 1'b0, bg_g3 };
   Bit#(4) bg_r4 = { 1'b0, bg_r3 };
   Bit#(4) bg_b4 = { 1'b0, bg_b3 };

次にこのようにMSBに0を詰めて各色4bitとします。

   // 背景無効領域は完全黒
   Bit#(4) pix_g4 = bg_active ? mixPx(bg_g4, fg_g1) : 4'b0000;
   Bit#(4) pix_r4 = bg_active ? mixPx(bg_r4, fg_r1) : 4'b0000;
   Bit#(4) pix_b4 = bg_active ? mixPx(bg_b4, fg_b1) : 4'b0000;

前景と背景をブレンドしてdisplay timingでレターボックス化します。以下は前景と背景の合成関数です。

// 背景4bit bg4 と 1bit 前景 fg1 を合成
// 前景の足し込み量は 4'hC 固定(強すぎれば 8〜F で調整)
function Bit#(4) mixPx(Bit#(4) bg4, Bit#(1) fg1);
   UInt#(4) ubg  = unpack(bg4);
   // fg1 が 0 のとき 0000, 1 のとき 1111
   Bit#(4) add_b = 4'hC & { fg1, fg1, fg1, fg1 };
   UInt#(4) uadd = unpack(add_b);
   UInt#(5) sum  = zeroExtend(ubg) + zeroExtend(uadd);
   UInt#(4) out4 = (sum > 15) ? 15 : truncate(sum);
   return pack(out4);
endfunction

図1048.1に完成結果を示します。実際には動画で見るより背景画像は暗くなっており、ゲームの邪魔になることはありません。

図%%.1
図1048.1 完成画面

記事タイトルはGameFSMの改良ですが、実際に背景画像や星の点滅はGraphicsFSMというグラフィックコントローラに実装しました。背景画像が結構ROMを食うため、Arty 7-35Tでは入らず、Arty 7-100Tでなければ入りませんでした。


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

GameFSMの改良 (22)

posted by sakurai on December 10, 2025 #1047

オリジナルのゲームのうち、アップライト型は前方の背景と下方のブラウン管映像がハーフミラーで合成されています。今回はその効果をFPGAで表現したいと思います。

まず背景画像はここで取得します。画像が大きいので、400x295x12bitにダウンサイズします。$(g_3, g_2, g_1, g_0, r_3, r_2, r_1, r_0, b_3, b_2, b_1, b_0)$という各色4bitカラーの12bitとします。

図%%.1
図1047.1 背景画像

ただし、背景は前景の邪魔にならないように暗めにαブレンディングするため、結果のMSBは常に0となります。従ってαブレンディング後のデータをROMに格納し、MSBを0として使用することにすれば、各色3bitの画像ですみます。

背景画像の前処理

  • γ補正
  • 全体的な減光 を掛けたうえで 3bit に量子化する。 $$ q_3 = \left\lfloor d \cdot \left(\frac{x}{255}\right)^{\frac{1}{\gamma}} \cdot 7 \right\rfloor $$ ここで
  • $x$:0..255 の元ピクセル値
  • $\gamma$:例えば$1.6$
  • $d$:暗さを決める係数、例えば$0.4$
  • $q_3$:0..7 の 3bit コード
  1. ROM には GRB333 で格納する。 $$ \text{word} = 64 \cdot G_3 + 8 \cdot R_3 + B_3 $$

デコーダ側は、 $$ \text{out} = \min\bigl(\text{bg} + \text{fg} \cdot A,\ 15\bigr) $$

  • $\text{bg}$:背景の 4bit 値(0..15) → 実際には MSB=0 なので 0..7 の範囲
  • $\text{fg}$:前景ビット(0 または 1)
  • $A$:前景の「足し込む強さ」(いまは (A = 12 = 4'hC))

  • fg1 = 0 のとき

    • {fg1,fg1,fg1,fg1} = 0000
    • add_b = 4'hC & 4'b0000 = 0
    • out = min(bg + 0, 15) = bg  → 背景だけ(透過光だけ)
  • fg1 = 1 のとき

    • {fg1,fg1,fg1,fg1} = 1111
    • add_b = 4'hC & 4'b1111 = 4'hC
    • out = min(bg + 12, 15) → 背景に「反射光分 12段」を足して、4bit 上限 15 を超えたら飽和

という「足し算+クリップ」によりハーフミラー効果を再現しています。


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

GameFSMの改良 (21)

posted by sakurai on December 4, 2025 #1046

夜空に瞬く星をシミュレーションする星の点滅プログラムです。

Advent Calenderで発表の都合上その後に公開します。 $$\img[-1.35em]{/images/withinseminar.png}$$

ファイルstars_xy.vhは、サポートプログラムにより元画像から白点の位置を抜き出したデータであり、以下のようなものです。コメントにもありますが、 $$(x, y, P, \phi)$$ の形式です。これをStringでChatGPTに改善してもらったように{ps1, ps2, ...., psn}と並べます。 packed star形式としてps関数を用いて以下のように初期化し、

// loaded 76 stars from stars_xy.csv
// ps(x, y, P, phi) 形式
  ps(  0,   0, 64, 19),
  ps(204,   1, 16,  7),
  ps( 63,   2, 16, 11),
:
  ps( 33, 251, 32,  8),
  ps(165, 254,  8,  0),
  ps(232, 254, 64, 33)

使う場合はあらかじめunpackしてreverseしたテーブルのほうを引いて使います。

ChatGPT 5.1に各種サポートプログラムをpythonで作成してもらった他、bsvについて 9割方はChatによるもので ユーザはデバッグ係となっていました。

ChatGPT 5.1は慣れないbsvこそ時々文法誤りコードを吐くことがあるものの、pythonは完璧な出来映えで一度もバグを入れることはありませんでした。pythonの方が得意な印象ですが、世の中に有るコードベースを考えたら無理もありません。

これ程pythonが得意なら。今後もう人間がpythonを書くことは無くなり、pythonは機械語のような位置付けになるかもしれません。昔は機械語のバイナリを覚えたものですが(C3,00,80,...)それと同じことになりそうです。

幸いbsvはまだそこまで得意ではないので、まだ人間が書く楽しみが残されています。


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

GameFSMの改良 (20)

posted by sakurai on December 3, 2025 #1045

次に夜空の星の点滅プログラムです。8bitレトロマシンとは思えないくらいの立体感のある映像になりました。もともと立体感のある画像だからなのですが。

例によって星の点滅をChatGPTと相談したら3案提示されたのですが、周期と位相を適当にずらす案を採用し、そのコードを書きました。

案3:座標から「周期と位相」を決めるだけの簡単版

擬似乱数すら使わず、各星に違う周期と位相を与えるだけでも、それなりに「ランダムっぽく」見えます。

  1. フレームカウンタ$t$を用意。

  2. 星の座標$(x,y)$から、その星固有の周期$P$と位相$\phi$を決める。 例: $$ P = 8 + ((x + 2y) \bmod 5) \quad (8 \sim 12; \text{フレーム周期}) $$ $$ \phi = (3x + y) \bmod P $$

  3. 星は次の条件で ON にする。 $$ \text{star_on} = 1 \iff ((t + \phi) \bmod P) < \frac{P}{2} $$

  • つまり各星は周期$P$で点滅し、そのうち半分だけ点灯。
  • 星ごとに$P$と$\phi$が違うため、全体としてはかなりバラバラに光って見える。

ハード実装としては

  • $P$を事前に ROM に入れておくか、座標から組み合わせ回路で計算、
  • Pを2の冪にすれば、$\bmod P$の演算も不要

Advent Calenderで発表の都合上その後に公開します。 $$\img[-1.35em]{/images/withinseminar.png}$$

図1045.1 オープニング画面 星空

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

GameFSMの改良 (19)

posted by sakurai on December 1, 2025 #1044

ブリッジのコードに怪しいところがあるので、修正中です。さて、オープニング画面を追加したいと思い、bsvコードを書きました。8bit(?)レトロマシンとは思えないくらいの立体感のある映像になりました。

まず画像のロード関数です。元画像はここにあった解像度の高いものを256x256に縮め、さらに色数を4色に落としました。単純にパターンROMに画像を置いてVRAMに転送するだけではあまり面白くないので、RLE (run length encoding)を行いました。

元画像はpythonでRLEしましたが、そのデコーダ部のbsvコードを示します。

// PROM アドレス(1 nibble 単位)
Reg#(PAddr_t)   rleAddr      <- mkReg(0);

// 展開先の相対座標 (0..255, 0..255)
Reg#(U8)        rleX         <- mkReg(0);
Reg#(U8)        rleY         <- mkReg(0);

// 残りピクセル数 256 * 256
Reg#(UInt#(16)) pixelsLeft   <- mkReg(0);

// 現在のランの残り長さ (1..256)
Reg#(UInt#(9))  runLen       <- mkReg(0);

// 色と長さ nibble
Reg#(Pattern_t) rleColor     <- mkReg(0);
Reg#(Pattern_t) rleLenHiNib  <- mkReg(0);
Reg#(Pattern_t) rleLenLoNib  <- mkReg(0);

// PROM から nibble(Pattern_t) を1つ読み取り、dst に入れ、rleAddr を 1 進める
function Stmt rleNextNibble(Reg#(Pattern_t) dst);
  return (seq
    action
      // PROM アドレスをセット
      p_addr <= rleAddr;
    endaction
    // read timing 調整が必要ならここに noAction を挟む
    noAction;
    action
      // nibble を取り込み
      dst     <= romdata;      // romdata : Pattern_t
      rleAddr <= rleAddr + 1;  // 次の nibble へ
    endaction
  endseq);
endfunction

// PROM 上で RLE が始まるニブルアドレス(行256の先頭なら 128*256)
`define RLE_START_ADDR 128*256
`define VRAM_WIDTH     256
`define VRAM_HEIGHT    256
   
function Stmt rleDecode_org();
  return (seq
    //------------------------------------------------
    // 初期化
    //------------------------------------------------
    action
      // RLE データは PROM の 256 行目から始まる前提
      rleAddr    <= fromInteger(`RLE_START_ADDR);
      // 出力先相対座標
      rleX       <= 0;
      rleY       <= 0;
      pixelsLeft <= fromInteger(`VRAM_WIDTH * `VRAM_HEIGHT -1);
      runLen     <= 0;
    endaction
    //------------------------------------------------
    // 全ピクセルを描き終えるまでループ
    //------------------------------------------------
    while (pixelsLeft != 0) seq
      // --- 新しいランを読み込む必要があるなら ---
      if (runLen == 0) seq
        // color
        rleNextNibble(rleColor);
        // len_hi
        rleNextNibble(rleLenHiNib);
        // len_lo
        rleNextNibble(rleLenLoNib);
        action
          UInt#(4) hi = unpack(rleLenHiNib);
          UInt#(4) lo = unpack(rleLenLoNib);
          // ラン長 L = 16*hi + lo + 1
          runLen <= extend(unpack({pack(hi), pack(lo)})) + 1;
        endaction
      endseq
      // --- このランから 1 ピクセル描画 ---
      setDot(rleX, rleY, rleColor);
      action
        pixelsLeft <= pixelsLeft - 1;
        runLen     <= runLen     - 1;
        // 256 幅で折り返し
        if (rleX == 8'd255) begin
          rleX <= 0;
          rleY <= rleY + 1;
        end
        else begin
          rleX <= rleX + 1;
        end
      endaction
    endseq   // while (pixelsLeft != 0)
  endseq);
endfunction
FSM rle_fsm <- mkFSM(rleDecode());

// rleDecodeを起動するラッパ
function Stmt rleDecode();
  return (seq
    `RUN_FSM(rle_fsm)
  endseq);
endfunction

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

GameFSMの改良 (18)

posted by sakurai on November 21, 2025 #1043

過去記事の続きです。GameFSM(ゲームシナリオ)とSoundFSM(サウンドプレーヤ)の間をOneStageというセマフォで接続していて、それを最適化(除去)しようとしたところ、ChatGPTにCDCを考慮していないと怒られてしまいました。「なら作って」と言って同期化ブリッジを作ってもらいました。実際にはやり直しが何度もありましたが。

マルチクロック設計で、2つの非同期クロックドメインにまたがる1段の同期化FIFOを用いています。

図%%.1
図1043.1 同期化FIFO

作ってもらったコードを示します。

GSBridge.bsv:

package GSBridge;

import Clocks::*;

typedef Bit#(4) SoundCode_t;

// Game 側 IF:busy を見て !busy のときだけ setReq する前提
interface GSBridgeGameIfc;
  method Action setCode(SoundCode_t code);   // このサイクルのコード値
  method Action setReq (Bool fire);          // このサイクルで発行するなら True
//  method Bool   busy;                        // バッファ占有中なら True
endinterface

// Sound 側 IF:valid が立ったサイクルで code を取り込む
interface GSBridgeSoundIfc;
  method SoundCode_t code;    // 取り込まれたコマンド値
  method Bool        valid;   // 新コマンド到着 1 サイクルパルス
endinterface

interface GSBridgeIfc;
  interface GSBridgeGameIfc  game;
  interface GSBridgeSoundIfc sound;
endinterface
(* synthesize, always_ready, always_enabled, no_default_clock, no_default_reset *)
module mkGSBridge#(Clock gameClk, Reset gameRst,
                   Clock sndClk)
                  (GSBridgeIfc);

  // gameRst を sndClk ドメインに同期させたリセット
  Reset sndRst <- mkSyncReset(2, gameRst, sndClk);

  // Game→Sound の 4bit コマンド用 Sync FIFO(深さ1)
  SyncFIFOIfc#(SoundCode_t) fifo
    <- mkSyncFIFO(1, gameClk, gameRst, sndClk);

  // Game ドメイン側入力(必ず gameClk/gameRst にぶら下げる)
  Wire#(SoundCode_t) w_code <- mkWire(clocked_by gameClk, reset_by gameRst);
  Wire#(Bool)        w_req  <- mkWire(clocked_by gameClk, reset_by gameRst);

  // Sound ドメイン側の出力レジスタ(sndClk/sndRst ドメイン)
  Reg#(SoundCode_t) r_code  <- mkRegU   (clocked_by sndClk, reset_by sndRst);
  Reg#(Bool)        r_valid <- mkReg(False,           clocked_by sndClk, reset_by sndRst);

  //-------------------------
  // Game ドメイン: fire かつ FIFO に空きがあるときだけ enq
  //-------------------------
  rule rl_enq (w_req && fifo.notFull);
    fifo.enq(w_code);
  endrule

  //-------------------------
  // Sound ドメイン: FIFO から 1 件取り出して r_code にラッチ
  //                 valid を 1 サイクルだけ立てる
  //-------------------------
  rule rl_deq (fifo.notEmpty && !r_valid);
    r_code  <= fifo.first;
    fifo.deq;
    r_valid <= True;
  endrule

  rule rl_clear (r_valid);
    r_valid <= False;
  endrule

  //-------------------------
  // Game 側サブインタフェース実装
  //-------------------------
  interface GSBridgeGameIfc game;

    // このサイクルのコード値を保持
    method Action setCode(SoundCode_t code);
      w_code <= code;
    endmethod

    // このサイクルで発行するなら fire=True
    method Action setReq(Bool fire);
      w_req <= fire;
    endmethod

    // ★ busy は「未消費のコマンドが FIFO にあるかどうか」
    //    = 元の 1bit セマフォと等価
//    method Bool busy;
//      return fifo.notEmpty;
//    endmethod

  endinterface

  //-------------------------
  // Sound 側サブインタフェース実装
  //-------------------------
  interface GSBridgeSoundIfc sound;

    // 直近に取り込んだコマンド値(valid が 1 のサイクルに有効)
    method SoundCode_t code;
      return r_code;
    endmethod

    // 新コマンドが届いたサイクルだけ 1 になるパルス
    method Bool valid;
      return r_valid;
    endmethod

  endinterface

endmodule

endpackage

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

GameFSMの改良 (17)

posted by sakurai on October 24, 2025 #1039

3番目にボディが厚い関数の最適化をトライします。対象はinitAll()(毎回のステージでの初期化関数)で、45行あります。

初期化関数(initAll())の最適化前後 比較
BSV合成 コンパイル時間 1'03'' 0'54'' ▲14.3%
Verilog合成 ファイルサイズ[KB] 4,283 3,554 ▲17.0%
合成時間 0'58'' 0'51'' ▲12.1%
Vivado LUT数 5,582 5,489 ▲1.7%
Vivado FF数 1,907 1,907 0.0%

結果としては、FSMオーバヘッドはほぼ0でした。一方、ボディがやや厚かったため、bscコンパイル時間とverilog量の削減となりました。物量も削減されているのは2度呼ばれているためかもしれません。bscコンパイル時間に効果があるため、この最適化を採用します。

かつて1時間以上かかっていたbscコンパイルがPCやbscの更新により16分になり、さらに今回のFSM切り出しにより、とうとう1分を切るようになりました。まさに隔世の感があります。

最初は論理の構築に集中しているため最適化までは手が出せず、コンパイル時間が長いため思うように検証を進められませんでした。

一方、現在は論理が固まったことで最適化も容易になり、コンパイル時間は劇的に短縮されました。皮肉なものですがこの効果を当初に得られていればと感じます。


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

GameFSMの改良 (16)

posted by sakurai on October 21, 2025 #1038

2番目にボディが厚い関数の最適化をトライします。対象はupdatePlayerBullet()(自弾処理関数)で、64行あります。

自弾処理関数(updatePlayerBullet())の最適化前後 比較
BSV合成 コンパイル時間 1'15'' 1'03'' ▲16.0%
Verilog合成 ファイルサイズ[KB] 5,253 4,283 ▲18.5%
合成時間 0'52'' 0'58'' 11.5%
Vivado LUT数 5,490 5,582 1.7%
Vivado FF数 1,894 1,907 0.7%

結果としては、おなじくFSMオーバヘッドが想定されそのとおりになりました。一方、ボディが厚かったため、bscコンパイル時間とverilog量の削減となりました。物量は微増していますが、bscコンパイル時間を1分を切らせたかったのでこの最適化を採用します。


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

GameFSMの改良 (15)

posted by sakurai on October 17, 2025 #1037

前回の1度しか呼ばれていない関数のdrawString関数はボディが薄い(関数の行数が少ない)ので効果が出なかったかもしれないと思い、ボディが厚い関数をトライします。対象はupdateAlienBullet()(敵弾処理関数)で、行数は83行あります。

敵弾処理関数(updateAlienBullet())の最適化前後 比較
BSV合成 コンパイル時間 1'27'' 1'15'' ▲13.8%
Verilog合成 ファイルサイズ[KB] 5,922 5,253 ▲11.3%
合成時間 0'59'' 0'52'' ▲11.9%
Vivado LUT数 5,685 5,490 ▲3.4%
Vivado FF数 1,789 1,894 5.9%

結果としてはまず1度しか呼ばれない関数のため、物量削減どころかFSMオーバヘッドが増加しました。一方、ボディが厚かったため、bscコンパイル時間とverilog量及びそれに比例するvivado合成時間が削減されました。

物量は増えたもののbscコンパイル時間が減ったため、この最適化を採用します。


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

GameFSMの改良 (14)

posted by sakurai on October 16, 2025 #1036

call順位が高い関数のFSM化が完了したので、次に1度しか呼ばれていない関数もFSM化してみます。これは物量にはほぼ影響はないか若干増加するものの、関数のシーケンスを巨大なシーケンスループからはずすことで、コンパイル時の競合条件計算量の減少を目的とするものです。まず、6個あるdrawTitle関数の1つをFSM化します。

まず、オリジナルのコードは、

   function Stmt drawTitle1(); // PLAY SPACE INVADERS
      return (seq
         for (str_idx <= 0; str_idx < 19; str_idx <=  str_idx + 1) seq
            copyGlyph(s1[str_idx]);
            waitTicks(`TICK_WAIT8);
            if (fbutton) break;
         endseq // for
      endseq);
   endfunction

コードの修正法は前回と同様なので省略します。

以下に結果の表を示します。思ったほどはコンパイル時間は減りませんでした。またverilog量は若干減ったものの、物量は若干増加しています。これは新たにFSMの起動、終了待ちが増えるためでしょう。

タイトル文字表示1(drawTitle1())の最適化前後 比較
BSV合成 コンパイル時間 1'26'' 1'25'' ▲1.2%
Verilog合成 ファイルサイズ[KB] 5,922 5,790 ▲2.2%
合成時間 0'59'' 0'53'' ▲10.2%
Vivado LUT数 5,583 5,638 1.0%
Vivado FF数 1,784 1,794 0.6%

全体としてあまり意味が無さそうなのでこの最適化は撤回します。


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


ページ: