Posts Tagged with "space invaders"

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

2. サブFSMの起動

サブFSMを1個だけインスタンスし、呼び出し側はstartとdoneだけを触る、という構造は8/3版と同じです。RUN_FSMマクロも、メインに置くwt_fsmやinitAll_fsmなど9個のFSMでは今も使っています。変わったのはブリッタの呼び方で、引数のラッチと起動が呼び出し側の2つの文からモジュールの中に移りました。

書き換え前

Reg#(Blit) blit_arg      <- mkReg(Blit{
  op:  BLIT_COPY,   // 無難なデフォルト
  sx:  0,  sy:  0,
  dx:  0,  dy:  0,
  w:   0,  h:   0,
  col: 0
});   // blit 用の引数保持レジスタ

FSM gfx_fsm <- mkFSM(blit(blit_arg));

// RUN_FSM: サブFSM 起動マクロ(start → done 待ち)。seq - endseq 内で呼ぶこと
`define RUN_FSM(F) action F.start(); endaction await(F.done);

// runBlit: gfx_fsm の起動ラッパ
//   引数を blit_arg にラッチしてから gfx_fsm を start し、done を待つ。
function Stmt runBlit(Blit b);
  return (seq
    action blit_arg <= b; endaction
      seq
        `RUN_FSM(gfx_fsm)   // start→done
      endseq
  endseq);
endfunction // runBlit

  function Stmt copyArea(U8 sx, U8 sy, U8 dx, U8 dy, U9 w, U9 h);
    return runBlit(Blit{op:BLIT_COPY, sx:sx, sy:sy, dx:dx, dy:dy, w:w, h:h, col:0});
  endfunction // copyArea

書き換え後、Blitter.bsv側

// BlitPort: ブリッタの利用者に見せる面
interface BlitPort;
   method Action start(Blit b);     // コマンド投入(1 サイクル後に FSM 起動)
   method Bool   done;              // 完了(コマンド未投入時も True)
   method Bool   obj;               // 直前の VDOTS/HDOTS でスプライト画素あり
   method Bool   base;              // 直前の VDOTS/HDOTS でシールド画素あり
endinterface

// runBlitOn と同名ラッパ群の On 版: BlitPort を第 1 引数に取る。
// 各モジュールでは let copyArea = copyAreaOn(port); のように部分適用して使う。
function Stmt runBlitOn(BlitPort p, Blit b);
  return (seq
    p.start(b);
    await(p.done);
  endseq);
endfunction // runBlitOn
function Stmt copyAreaOn(BlitPort p, U8 sx, U8 sy, U8 dx, U8 dy, U9 w, U9 h);
  return runBlitOn(p, Blit{op:BLIT_COPY, sx:sx, sy:sy, dx:dx, dy:dy, w:w, h:h, col:0});
endfunction // copyAreaOn
FSM blit_fsm <- mkFSM(blitDispatch(blit_arg));

   // ---- ブリッタの起動 ----
   // port.start はコマンドを RWire に載せるだけで、同じサイクルに kick が引数を
   // blit_arg にラッチし、次のサイクルに launch が blit_fsm を起動する。
   // 元の runBlit(引数ラッチ → start → done 待ち)と同じサイクル数になる。
   Reg#(Bool)   go  <- mkReg(False);
   RWire#(Blit) cmd <- mkRWire;

   rule kick (cmd.wget matches tagged Valid .b);
      blit_arg <= b;
      go <= True;
   endrule

   rule launch (go);
      go <= False;
      blit_fsm.start;
   endrule

   interface BlitPort port;
      method Action start(Blit b) if (!go && blit_fsm.done);
         cmd.wset(b);
      endmethod
      method Bool done  = !go && blit_fsm.done;
      method Bool obj   = fobj;
      method Bool base  = fbase;
   endinterface

書き換え後、GameFSM.bsv側

 Blitter_ifc blt <- mkBlitter;

// 原実装と同名の薄ラッパ群: Blitter.bsv の On 版に blt.port を部分適用
let copyArea    = copyAreaOn(blt.port);
let orArea      = orAreaOn(blt.port);
let eraseArea   = eraseAreaOn(blt.port);
let eraseAreaSP = eraseAreaSPOn(blt.port);
let readVdots   = readVdotsOn(blt.port);
let readHdots   = readHdotsOn(blt.port);

呼び出し箇所のcopyAreaの書き方は変わりません。変わるのは展開結果で、8/3版では呼び出し1箇所が「ラッチ、start、await」の3状態に展開されていたのが、教科書版では「start、await」の2状態になります。ラッチと起動の2サイクルはモジュールの中のkickとlaunchが担うので、動作のサイクル数は同じです。Verilatorで1,000万サイクルのVRAM書き込み系列を比べ、サイクル単位で一致することを確認しました。

生成Verilogで見ると、呼び出し箇所の規則はこう変わります。書き換え前は1箇所につきラッチの規則とawaitの規則が別々にあります。

// rule RL_action_l731c20_12       ← blit_arg <= b のラッチ
assign WILL_FIRE_RL_action_l731c20_12 =
    NOT_wt_fsm_jj_repeat_count_read__86_EQ_0_87_88_ETC___d895 &&
    !wt_fsm_start_reg &&
    state_mkFSMstate == 8'd46 ;

// rule RL_action_l731c59_9        ← await(gfx_fsm.done)
assign WILL_FIRE_RL_action_l731c59_9 =
    blit_arg_2_BITS_56_TO_54_3_EQ_0_4_AND_gfx_fsm__ETC___d954 &&
    state_mkFSMstate == 8'd43 ;

書き換え後はstartの規則とawaitの規則だけで、doneはモジュールの出力ポート1本になります。

// rule RL_action_l55c6_1          ← blt.port.start(b)
assign WILL_FIRE_RL_action_l55c6_1 =
    blt$RDY_port_start && str_idx_129_ULT_24___d5130 &&
    (state_mkFSMstate == 7'd3 || state_mkFSMstate == 7'd6) ;

// rule RL_action_l56c10_1         ← await(blt.port.done)
assign WILL_FIRE_RL_action_l56c10_1 =
    blt$port_done && state_mkFSMstate == 7'd4 ;

8/3版と教科書版で、各FSMの状態数は次のように変わりました。状態数は、生成Verilogの中で状態レジスタとの比較に現れる値の種類を数えたものです。教科書版の値には、4点目でメインからupdateAliens、updatePlayer、updateSaucerを切り出した効果と、3点目でブリッタの矩形4種のループを1組に統合した効果も含みます。教科書版ではgfx_fsmをblit_fsmに改名し、mkBlitterの中に置いています。

FSM 8/3版 教科書版 比較
メイン 193 90 ▲103
updatePlayerBullet_fsm 64 54 ▲10
updateAlienBullet_fsm 41 33 ▲8
initAll_fsm 28 22 ▲6
drawLives_fsm 13 9 ▲4
drawScores_fsm 26 18 ▲8
gfx_fsm、教科書版ではblit_fsm 47 22 ▲25

メリットをまとめると、呼び出し側からblit_argとgfx_fsmが消えてstartとdoneだけを見ればよくなったこと、そして呼び出し箇所ごとの状態が1つ減ったこと、の2つです。


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

posted by sakurai on September 19, 2026 #1124

ChatGPT版との差分3点

比較の前は8/3版で、記事#1026から記事#1028でChatGPTが描画関数を1個のサブFSMにまとめてstartとdoneで呼ぶ形にし、そのとき作ったRUN_FSMマクロを使って記事#1035から記事#1039でdrawLives、updateAlienBullet、updatePlayerBullet、initAllを順にFSMにした状態を指します。Claude版はその構造を保ったまま3箇所を書き換え、その上で記事#1035から記事#1039のFSM化を残りのseqに進めたものを4点目としました。以下、各点について書き換え前後のコードと、生成Verilogで確認できる差を示します。数値は、bscのコンパイル時間とVivado 2026.1のxc7a35t向けOOC合成結果を同じPCで測ったもので、LUTとFFにYosysと断ったものは手元の概算です。コードは教科書版に揃えてあります。

1. 文字列テーブル

ChatGPT版はビット連接でBit型の定数を作り、unpackしてVectorに戻していました。連接では先頭の要素が最上位ビット側に置かれ、unpackでは最下位ビット側が要素0になるので、順序を戻すreverseが必要でした。

書き換え前

   // str_over: "GAME_OVER"
   Bit#(SizeOf#(Vector#(9, Glyph))) str_over_bits = {
     pg(  2,186, 89,59, 5, 7), // G
     pg( 10,186, 97,59, 5, 7), // A
     pg( 18,186,105,59, 5, 7), // M
     pg( 26,186,113,59, 5, 7), // E
     pg( 34,186,121,59, 5, 7), // _
     pg( 42,186,129,59, 5, 7), // O
     pg( 50,186,137,59, 5, 7), // V
     pg( 58,186,145,59, 5, 7), // E
     pg( 66,186,153,59, 5, 7)  // R
   };
   Vector#(9, Glyph) str_over = reverse(unpack(str_over_bits));

書き換え後

   // str_over: "GAME_OVER"
   Glyph str_over[9] = {
     mkGlyph(  2,186, 89,59, 5, 7), // G
     mkGlyph( 10,186, 97,59, 5, 7), // A
     mkGlyph( 18,186,105,59, 5, 7), // M
     mkGlyph( 26,186,113,59, 5, 7), // E
     mkGlyph( 34,186,121,59, 5, 7), // _
     mkGlyph( 42,186,129,59, 5, 7), // O
     mkGlyph( 50,186,137,59, 5, 7), // V
     mkGlyph( 58,186,145,59, 5, 7), // E
     mkGlyph( 66,186,153,59, 5, 7)  // R
   };

左辺を配列型で宣言すると{ ... }は配列の初期化子として解釈され、先頭に書いた要素が添字0になります。pgとpack、unpack、reverseは要りません。mkGlyphはChatGPT版にもあった構造体のコンストラクタで、そのまま使っています。

生成Verilogは、行番号に由来する内部識別子を正規化するとdiffが0行でした。LUTとFFは増減なしです。定数の並べ方は静的展開の前にしか存在せず、展開後の回路には残りません。


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

GameFSMの改良 (25)

posted by sakurai on August 6, 2026 #1100

次に原作にもある「化石」という症状をインプリしてもらいました。

化石の動画

仕様: 化石の再現 隊列がシールド帯まで降下した状態で、左端での下降パス中に自弾が右端シールドに命中すると、これを個体命中と誤認する。シールド内の弾の位置に敵爆発音と敵爆発マークを発生させ、マーク消去時にシールドへ 16×8 の穴を開ける一方、実際に死ぬのは弾の真上とは別の列の最前列個体である。その個体の画像は消去されないまま画面に残り、以後は敵として撃てず、隊列だけが先へ進む。

発症条件の幾何定数 — 隊列の降下判定しきい値と、右端シールドの x 範囲。

`ifdef FOSSIL_BUG
`define FOSSIL_BUG_Y 176   // 原作バグ発症条件: 隊列がシールド上端(192)の1ピッチ以内                            
`define FOSSIL_BUG_X 182   // これ以右のシールド命中で発症(=右端シールド全体、x=182〜204)                       
`endif

爆発マークの描画・消去位置を保持する — 犠牲個体の座標から切り離すことで「マークと犠牲が別の場所」という原作バグの本質を表現可能にする。

`ifdef FOSSIL_BUG
   Reg#(UInt#(8)) expl_x <- mkRegU,   // インベーダ爆発マークの描画・消去位置                                   
                  expl_y <- mkRegU;   // (バグ再現時は個体位置とマーク位置が食い違う)                           
`endif

常キル時にマーク位置=個体位置を expl_x/expl_y へラッチする(挙動は従来と同一の中立改造)。

`ifdef FOSSIL_BUG
               eraseArea(
               expl_x,
               expl_y,
               16, 8);
`else
               eraseArea(
               inv_x[gx][gy],
               inv_y[gx][gy],
               16, 8);
`endif

マーク消去を個体座標ではなく expl_x/expl_y に対して行う(バグ時はシールド内を消して穴を開ける)。

`ifdef FOSSIL_BUG
      return (seq
         action
            alien_timer <= 1;
            expl_x <= inv_x[gx][gy];   // マーク位置=個体位置(通常キル)                                         
            expl_y <= inv_y[gx][gy];
         endaction
         copyArea(96, colorAtY(inv_y[gx][gy])*16+16,
                  inv_x[gx][gy], inv_y[gx][gy],
                  16, 8);
      endseq);
`else
      return (seq
         alien_timer <= 1;
         copyArea(96, colorAtY(inv_y[gx][gy])*16+16,
                  inv_x[gx][gy], inv_y[gx][gy],
                  16, 8);
      endseq);
`endif

発症4条件成立時のシールド命中を誤認キル化する本体 — マークと音は弾の位置、死ぬのは左端側の生存列の最前列、絵は残って化石になる。

`ifdef FOSSIL_BUG
               endseq else if (fbase && ymax >= `FOSSIL_BUG_Y
                               && inv_move == MoveLeftDown
                               && bullet_x >= `FOSSIL_BUG_X) seq
                  // ---- 原作バグの再現(希少条件版) ----                                                       
                  // 右→左で来て左端で下降中(=MoveLeftDown、隊列スキュー中)、                                  
                  // かつ隊列がシールド帯まで降りた状態で右端シールドを撃つと、                                 
                  // 原作の座標→個体逆算の破綻(左下原点からの列計算オーバー                                    
                  // フローが隣行へ回り込む)を模して、左端の生存個体が誤って                                    
                  // 殺される。マークと消去は弾の位置、犠牲の絵は左端に無傷で                                   
                  // 残り、隊列に取り残される(化石)。                                                           
                  fhit_alien <= False;
                  for (gx <= 0; gx < `InvCols; gx <= gx + 1) seq
                     if (inv_s[gx][inv_lead_y[gx]]) seq   // 左端の生存列を探す                                 
                        fhit_alien <= True;
                        break;
                     endseq
                  endseq
                  if (fhit_alien) seq
                     gy <= inv_lead_y[gx];
                     erasePlayerBullet(bullet_x, bullet_y);
                     inv_s[gx][gy] <= False;
                     inv_no <= inv_no - 1;
                     updateColumnFrontY();
                     playSound(SND_ALIEN_EXPLODE);
                     action                        // マークは弾の位置に描く                                    
                        alien_timer <= 1;
                        expl_x <= bullet_x - 8;
                        expl_y <= bullet_y;
                     endaction
                     copyArea(96, colorAtY(bullet_y)*16+16,
                              bullet_x - 8, bullet_y, 16, 8);
                     action
                        score <= score + inv_score[gy];
                        bscore <= bscore + inv_score[gy];
                     endaction
                  endseq else seq
                     erasePlayerBullet(bullet_x, bullet_y);   // 生存列なし: 通常のシールド命中                 
                     explodePlayerBullet(35, bullet_x, bullet_y);
                     bullet_timer <= 1;
                  endseq
`endif

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

GameFSMの改良 (24)

posted by sakurai on August 3, 2026 #1099

最近は推論能力が高いため、もっぱらClaude Fable 5を使用しています。さてCaludeに以下の仕様を実装してもらいました。

「自弾がインベーダに命中すると、爆発マークの表示期間(ALIEN_EXPL_TMAX tick)のあいだ隊列全体の移動を停止する。停止中も自機・自弾・敵弾・UFO・サウンドの処理は継続し、爆発マークの消去とともに、停止した個体から隊列の移動を再開する。」

これはオリジナル動作を観察して発見した仕様です。理由は不明ながら、爆発マーク表示中も隊列を動かすと、インベーダの動きにより爆発マークが欠ける場合があり、その干渉を防止するためと考えられます。

            for (noy <= 0; noy < `InvRows; noy <= noy + 1) seq
               // 爆発凍結中はカーソルを進めない(増分が no-op になる)
               for (nox <= 0; nox < `InvCols;
                    nox <= (alien_timer == 0) ? nox + 1 : nox) seq
                  if (inv_s[nox][noy] || alien_timer != 0) seq
                     if (alien_timer == 0)
                        updateAliens();
                     else
                        eraseAlienExplosion();
                     updatePlayer();
                     ...(以下、元のまま)...
                  endseq  // if inv
               endseq // for nox
            endseq // for noy

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

bitmap editorの開発

posted by sakurai on July 28, 2026 #1098

Claude Fable 5に仕様を渡して表記エディタを開発して貰いました。AI以前はこのような小さなヘルパーツールでも、一週間程度はかけて作成及びデバッグしたものでしたが、改良も含めて10分程でできてしまうことに感激です。もちろんコードは一行も書いてません。

ここで仕様の一部を示すと、ベースのcsvを読ませ、エディタによりaパターンとbパターンの作成を行うと、自動的にプレビューでパターン間の動作を確認できます。OKなら最下段のOKボタンを押すと、ベースのデータとスプライトのデータが合成され、ROMデータのcsvとなります。スプライトcsvは別にロードセーブ可能です。

図1098.1
図1098.1 スプライトエディタ


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

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

図1048.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とします。

図1047.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

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

// pack ∘ st のラッパ (packed star)
function Bit#(SizeOf#(StarTwinkle)) ps(U8 x, U8 y, U8 p, U8 phi);
  return pack(st(x, y, p, phi));
endfunction

// 星テーブル (packed ビット列)
Bit#(SizeOf#(Vector#(NumStars, StarTwinkle))) starTwinkleBits = {
// ps(x, y, P, phi) 形式
`include "stars_xy.vh"
};

// Vector#(NUM_STARS, StarTwinkle) に変換
Vector#(NumStars, StarTwinkle) starTwinkleTable =
  reverse(unpack(starTwinkleBits));

// 1フレーム分、星の点滅を更新する Stmt
//   frameCnt : グローバルフレームカウンタ (60Hzで+1される想定)
//   baseX,baseY : 星描画の基準座標(ロゴ全体にオフセットを掛けたければここで指定)
function Stmt stepStarField();
  let s = starTwinkleTable[i];
  let phaseIdx = (truncate(counter) + s.phase) % s.period;
  let on = (phaseIdx < (s.period >> 1));
  return (seq
    for (i <= 0; i < fromInteger(valueOf(NumStars)); i <= i + 1) seq
      if (on)
        setDot(s.x, s.y, 7);
      else
        setDot(s.x, s.y, 0);
    endseq
  endseq);
endfunction

ファイル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) \gt \frac{P}{2} $$

  • つまり各星は周期$P$で点滅し、そのうち半分だけ点灯。

  • 星ごとに$P$と$\phi$が違うため、全体としてはかなりバラバラに光って見える。

    ハード実装としては

  • $P$を事前に ROM に入れておくか、座標から組み合わせ回路で計算、

  • Pを2の冪にすれば、$\bmod P$の演算も不要

図1045.1
図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

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


ページ: