Posts Tagged with "AI"

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

GameFSMの結果

posted by sakurai on September 26, 2026 #1131

Vivado 2026.1で合成したゲーム全体の各モジュールの物量を表1131.1に示します。ロジックはすべてBSVで記述し、トップ配線とメモリ、クロック生成のみVerilogで書いています。論理合成はトップからのフラット合成で、物量は階層別レポートから読みました。小さなモジュールはフラット合成で親に吸収され計上されるため、その分は親の残差として示し、注記します。

表1131.1 各モジュールの物量
モジュール 説明 BSV行数 生成Verilog行数※1 物量
LUT数 LUT割合[%] FF数 FF割合[%] BRAM数
mkGameFSM 自機、インベーダ、UFO、自弾、敵弾、スコア等を処理するFSM 1,554 31,439 2,904 48.7 1,807 73.0 -
mkBlitter VRAMへの矩形転送、消去、衝突判定を実行するFSM(mkGameFSMから分離) 335 1,406 2,073 34.8 128 5.2 -
mkSoundFSM3 UFO音を演奏するFSM 261※2 1,968 215 3.6 116 4.7 -
mkSoundFSM0 自弾発射音、自機爆発音、自機増加音を演奏するFSM 1,821 201 3.4 116 4.7 -
mkSoundFSM2 インベーダ歩行音を演奏するFSM 1,823 198 3.3 109 4.4 -
mkSoundFSM1 インベーダ爆発音を演奏するFSM 1,796 193 3.2 109 4.4 -
mkGraphicFSM VGA映像信号生成回路 196 410 54 0.9 33 1.3 -
mkParaSeri サウンドパラシリ変換、タイミング生成回路 77 251 23※3 0.4 39※3 1.6 -
mkOneStage ゲームFSMとサウンドFSMの間のバッファ 62 201
mkMixer 4チャネルのサウンドミキサー 59 133
mkButtons スイッチとボタンをORする回路 36 102 85※4 1.4 0 0 -
clock クロック制御回路 - 84 11※5 0.2 19※5 0.8 -
vram ビデオRAM - 34 - - - - 8
sound_rom0 自弾発射音、自機爆発音、自機増加音を格納するROM - 26※6 - - - - 8
sound_rom3 UFO音を格納するROM - - - - - 8
sound_rom1 インベーダ爆発音を格納するROM - - - - - 2
sound_rom2 インベーダ歩行音を格納するROM - - - - - 2
pattern_rom 自機、インベーダ、UFO、自弾、敵弾、スコア等の図形を格納するROM - - - - - 4
合計 5,958 100 2,476 100 32
  • ※1 bscが生成するVerilogには、$displayによるエラー・デバッグ表示やルール衝突検知など、合成時に除去されシミュレーションでのみ働く記述が含まれる。合成対象となるのは概ね半分程度で、この行数がそのまま記述の圧縮率を表すわけではない。
  • ※2 BSVのソースは1つで、defineにより0~3のバリエーションを生成する。
  • ※3 mkParaSeri、mkOneStage、mkMixerはフラット合成で親のsoundモジュールに吸収され計上されるため、階層別レポートに個別の行が立たない。3つの合計を、soundの物量から4つのサウンドFSMを引いた残差として示す。
  • ※4 mkButtonsは親のmasterモジュールに吸収されるため、masterの残差として示す。トップ配線のグルーロジックを含む。
  • ※5 clockはトップ直下に吸収されるため、トップの残差として示す。
  • ※6 ROM(rom.v)は共通のパラメタ化モジュールで、4つのサウンドROMとパターンROMが同一ソースを共有する。

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

posted by sakurai on September 24, 2026 #1129

まとめ

同じPCで、bsc 2025.07のコンパイル時間と、Vivado 2026.1で論理合成した結果を、描画のmodule化前後で比べます。前は8/3版、後は教科書版です。論理合成はArty A7のトップからのフラット合成で、階層別レポートからmkGameFSMの値を読みました。ソース行数はGameFSM.bsvとBlitter.bsvの合計、VerilogはmkGameFSM.vとmkBlitter.vの合計です。抑止した警告数は、前がG0036とG0010、後がそれにG0009を加えた数で、8/3版にG0009は出ません。合成対象は前後ともsvnのリビジョンで固定し、生成Verilogのmd5でWindowsへ渡した実物を照合しました。

描画モジュール化前後 前 後 比較
BSV合成 ソース行数[行] 1,878 1,889 0.6%増
コンパイル時間 0:51 0:29 ▲44%
抑止した警告数 1,734 451 ▲74%
生成Verilog行数[行] 47,117 32,845 ▲30%
Verilog合成 合成時間(トップ全体) 1:10 1:11 ほぼ同じ
Vivado LUT数 5,352 4,977 ▲7.0%
Vivado FF数 1,919 1,935 0.8%増

表の各行はmkGameFSMとmkBlitterの範囲の値です。ただし合成時間だけはトップ全体の値で、フラット合成ではmkGameFSMの合成時間を単独に取り出せないためです。母数が他の行と異なる点に注意が要ります。

コンパイル時間を決める構造で見ると、前後の差はBlitterのmodule化だけです。前の版は既にRUN_FSMマクロによる切り出しを済ませており、描画の実体をmkFSMで1本に集約した状態です。後の版はその集約したBlitterを、ヒット判定まで含めて独立したmoduleに出しています。

効果はbscのコンパイル時間に集中しています。ソース行数はほぼ同じで、同じ量を書いてコンパイルが半分になりました。生成Verilogが30%減り、抑止した警告が4分の1になったのは、GameFSM側から描画とヒット判定の展開が消えた分です。

物量はLUTが7.0%減、FFが0.8%増です。モジュール化そのものは規則とレジスタの置き場所を変えるだけで論理を変えないので、物量を減らす理由を持ちません。むしろmkBlitterの境界に置いたkickとlaunchの規則、RWire、goレジスタの分だけ増える方向で、FFの0.8%増がそれです。LUTが減ったのは、前後の間に入った他の改変の効果です。2点目の呼び出し形の変更と4点目のFSM切り出しで各FSMの状態が合計418から338に減り、80状態ぶんの状態デコードと遷移条件の論理が消えました。またブリッタの矩形4種のループを1組に統合し、gfx_fsmの47状態が22になって、4種それぞれが持っていたループ制御とカウンタ更新の論理が1組に共有されました。減り方が小さいのは、減ったのが状態機械の制御部分だけで、各状態の中で行う描画や判定やゲームロジックの計算は8/3版と同じだからです。論理合成時間は動きません。

コンパイル時間が縮む機序は、ステート干渉チェックの境界での打ち切りです。同一ソースにあるとき、bscはBlitterの状態とGameFSMの状態を一緒くたに解析し、すべての状態間・ルール間の干渉を調べます。module化すると、Blitter内の解析はモジュール境界で閉じ、GameFSM側から見えなくなります。GameFSMが解析する状態空間が小さくなり、その分の解析時間が消えます。抑止した警告の数が4分の1になったことが、解析対象の縮小を裏付けます。

FFがわずかに増えるのは、module境界のインタフェースレジスタの分です。起動コマンドをRWireに載せて1サイクル後にFSMを起動する形にしたため、引数のラッチと完了・判定の戻りがレジスタとして加わりました。この1サイクルの遅延はmodule化の実行時の代償で、描画1回あたり1サイクルです。

記事#1028の改良は、呼び出しごとに展開されていた描画ループを1個のサブFSMの再利用に変え、コンパイル時間を87%減らしました。記事#1035から記事#1039は、同じRUN_FSMでdrawLivesと、本体の厚いupdateAlienBullet、updatePlayerBullet、initAllを順に切り出し、1分54秒を54秒まで縮めました。

今回の改良は、ブリッタをモジュールの境界で包んだもので、bscのコンパイル時間を同じPCで51秒から29秒に、44%減らしました。3段階とも主眼はbscのコンパイル時間で、物量は付随して動きます。LUTは各段階で減り、記事#1028で18%、記事#1037から記事#1039で3%、今回で7.0%です。FFは記事#1028で0.5%減ったあと、記事#1037から記事#1039で7%、今回で0.8%増えました。seqをFSMに切り出すたびに状態レジスタと引数のラッチが増えるためで、FSM化はFFを増やす方向に働きます。


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

posted by sakurai on September 22, 2026 #1127

4. 1箇所からしか呼ばれないseqのFSM化

seqをFSMに切り出す第一の理由は再利用で、記事#1035のdrawLivesがそれです。6箇所から呼ばれるseqを1個のFSMにまとめ、コンパイル時間を25%減らしました。記事#1036からは別の狙いで、1箇所からしか呼ばれないseqでも巨大なメインのseqから外せば競合条件の計算量が減るはず、という試みが始まります。記事#1036のdrawTitle1は1度しか呼ばれず本体も薄く、コンパイル時間が1%しか動かなかったので撤回されました。記事#1037のupdateAlienBulletと記事#1038のupdatePlayerBulletは本体が厚く、コンパイル時間がそれぞれ14%と16%、Verilogが11%と19%減って採用され、記事#1039のinitAllも同様に採用されました。つまり効くかどうかは呼び出し回数ではなく本体の厚さで決まる、というところまでが8/3版の時点で分かっていました。

ここではその続きとして、同じ基準で残っていたupdateAliens、updatePlayer、updateSaucerの3つをFSMにしました。本体の厚いseqはメインから切り出す方がbscの負担が軽い、という記事#1037と記事#1038で見えていた傾向に従ったものです。動作はVRAM書き込みの系列がサイクル番号を除いて一致しました。

教科書版の最終形

以上から、教科書版は次の2つの基準で決めました。複数箇所から呼ばれるseqは、共有するためにFSM化します。1個のインスタンスを作り、RUN_FSMマクロで各所から呼びます。メインループから呼ぶ大きなseqは、1箇所からしか呼ばれなくても、メインを小さくするためにFSM化します。仕組みは同じで、動機が違います。前者は規則の複製を消し、後者はメインFSMの状態数を減らします。モジュール境界は、42箇所から呼ばれ自前のレジスタを持つブリッタだけに使います。

seq 呼び出し箇所 扱い 置き場所
copyAreaなど描画プリミティブ6種 42 共有するためのFSM化 mkBlitter
drawLives 6 共有するためのFSM化 メイン
waitTicks 4 共有するためのFSM化 メイン
initAll 2 共有するためのFSM化 メイン
drawScores 2 共有するためのFSM化 メイン
updateAliens、updatePlayer、updatePlayerBullet、updateAlienBullet、updateSaucer 各1 メインを小さくするためのFSM化 メイン
updateBonus、checkClear、erasePlayerBulletなど小さなseq 1から5 展開のまま メイン

FSMはmkBlitterの1個とメインの9個で、メインループは各サブシステムのFSMをstartしてdoneを待つだけの短い列になります。教科書版の各FSMの状態数は次のとおりです。

FSM 状態数
メイン 90
updatePlayerBullet_fsm 54
updateSaucer_fsm 36
updateAlienBullet_fsm 33
updatePlayer_fsm 30
initAll_fsm 22
blit_fsm、mkBlitterの中 22
drawScores_fsm 18
updateAliens_fsm 18
drawLives_fsm 9
wt_fsm 6
合計 338

メインFSMの状態数は8/3版の193から90になりました。FSM全体の合計では、8/3版の418から338です。コンパイル時間と物量の前後は記事#1129の表にまとめます。


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

posted by sakurai on September 21, 2026 #1126

3. モジュール化

8/3版では、ブリッタが使う作業レジスタと画面メモリ側のレジスタが全部メインモジュールの中で宣言されていました。メインのどの規則からも読み書きできるので、実質はグローバル変数です。

書き換え前、mkGameFSMの中

Reg#(PAddr_t) p_addr <- mkRegU;
Reg#(VAddr_t) v_addr <- mkRegU;

Reg#(Bool) fvwe <- mkReg(False),
           fwen <- mkReg(False),
           fgunexp <- mkReg(False),
           fbullet <- mkReg(False),
           fobj <- mkReg(False),
           fbase <- mkReg(False),
    fempty  <- mkReg(False),
           fhit_alien <- mkReg(False);

Reg#(SoundCode_t) inscode <- mkRegU;
Reg#(Pattern_t) vd <- mkRegU;
Wire#(Pattern_t) vdata <- mkWire,
                 romdata <- mkWire;
Wire#(UInt#(3)) dipsw <- mkWire;

Reg#(UInt#(9)) xs <- mkRegU,
               ys <- mkRegU,
               xd <- mkRegU,
 yd <- mkRegU;
method Action pdin(Pattern_t in_romdata);
   romdata <= in_romdata;
endmethod
method Action vdin(Pattern_t in_vdata);
   vdata <= in_vdata;
endmethod
method PAddr_t prom_addr();
   return p_addr;
endmethod
method VAddr_t vaddr();
   return v_addr;
endmethod
method Bool vwe();
   return fvwe;
endmethod
method Pattern_t vdout();
   return vd;
endmethod

書き換え後、Blitter.bsvのmkBlitterの中

// MemPort: 画面メモリ側の面
interface MemPort;
   (* always_ready *) method PAddr_t   prom_addr;
   (* always_ready *) method VAddr_t   vaddr;
   (* always_ready *) method Bool      vwe;
   (* always_ready *) method Pattern_t vdout;
   (* always_enabled *) method Action pdin(Pattern_t d);
   (* always_enabled *) method Action vdin(Pattern_t d);
endinterface

interface Blitter_ifc;
   interface BlitPort port;
   interface MemPort  mem;
endinterface

(* synthesize *)
module mkBlitter(Blitter_ifc);

   Reg#(PAddr_t) p_addr <- mkRegU;
   Reg#(VAddr_t) v_addr <- mkRegU;
   Reg#(Bool) fvwe <- mkReg(False),
              fobj <- mkReg(False),
              fbase <- mkReg(False);
   Reg#(Pattern_t) vd <- mkRegU;
   Wire#(Pattern_t) vdata <- mkWire,
                    romdata <- mkWire;
   Reg#(UInt#(9)) xs <- mkRegU,
                  ys <- mkRegU,
                  xd <- mkRegU,
                  yd <- mkRegU;
interface MemPort mem;
   method PAddr_t   prom_addr = p_addr;
   method VAddr_t   vaddr     = v_addr;
   method Bool      vwe       = fvwe;
   method Pattern_t vdout     = vd;
   method Action pdin(Pattern_t d); romdata <= d; endmethod
   method Action vdin(Pattern_t d); vdata   <= d; endmethod
endinterface

書き換え後、mkGameFSMの中

Blitter_ifc blt <- mkBlitter;
method Action pdin(Pattern_t in_romdata);
   blt.mem.pdin(in_romdata);
endmethod
method Action vdin(Pattern_t in_vdata);
   blt.mem.vdin(in_vdata);
endmethod
method PAddr_t prom_addr();
   return blt.mem.prom_addr;
endmethod
method VAddr_t vaddr();
   return blt.mem.vaddr;
endmethod
method Bool vwe();
   return blt.mem.vwe;
endmethod
method Pattern_t vdout();
   return blt.mem.vdout;
endmethod

レジスタの宣言は一字も変わらず、置き場所だけがmkBlitterに移りました。名前はインタフェースに現れないので、メインからは触れません。これがBSVにおけるローカル変数の実体で、関数の中で実行時の状態を持つ手段が無いため、状態の局所化はモジュールの単位で行います。mkBlitterに入れたのは、VRAMとPROMのアドレスを出してデータを読み書きする部分です。矩形転送の本体と、自弾や敵弾の進路のVRAMを読むヒット判定がそれで、MemPortに出ているprom_addr、vaddr、vwe、vdout、pdin、vdinを駆動する機構がここにあります。drawLivesやdrawScoresは、copyAreaやeraseAreaを呼ぶだけで自分ではアドレスを出さないので、ゲームロジックとしてメインに残しています。

ここで、2点目のRUN_FSMマクロによる切り出しと、3点目のモジュール化の違いを押さえておきます。マクロで包むと、ブリッタの中身は人間から見えなくなります。人間はFSMが1個ずつしか動かないことを知っているので、呼び出し側の完了待ちだけを見れば中身は気にしなくてよい、と判断できます。ところがbscはその仮定を知りません。マクロは展開されてメインFSMの文に戻り、gfx_fsmも作業レジスタもmkGameFSMの中にあるままなので、bscは全部の規則の間で読み書きの順序を総当たりで調べます。マクロで切り出すとインライン展開が消えて規則の総数は減りますが、残った規則どうしの総当たりは続きます。8/3版で警告が1,734件出ていたのはそのためです。

モジュール化は、この総当たりの範囲を境界で切ります。mkBlitterに移した規則とレジスタは、mkGameFSMをコンパイルするときの解析対象から外れ、bscが見るのはblt.portのstartとdoneだけになります。総当たりの計算量は規則の数の2乗のオーダーで増えるので、規則の総数が同じでも、範囲を2つに分ければ組の数は大きく減ります。マクロ化は規則の数を減らし、モジュール化は総当たりの範囲を切る、と対比できます。人間から見えなくするのがマクロ化、コンパイラから見えなくするのがモジュール化です。

物量への効果は、この段階だけを取り出すと2点目に書いた状態数の削減と同じもので、モジュールの境界そのものはLUTを増やしも減らしもしません。モジュール化が効いたのは別の2点です。

一つはコンパイル時間で、bscのスケジューラはメインの規則とブリッタの規則の組を調べなくなり、mkBlitterは独立に数秒でコンパイルされます。抑止した警告の数がそれを裏付けます。8/3版はG0036とG0010で1,734件でした。教科書版ではG0036が127件、G0010が318件に減ります。加えて教科書版ではG0009が出ます。別のFSMの状態どうしが同じレジスタを読み書きして順序の循環ができ、bscがその組をconflictにして循環を切った、という警告で、FSMは1個ずつしか動かないので実害はありません。G0036、G0010と同じ根拠で抑止します。教科書版ではこの循環を順に解消し、G0009は6件です。合計は1,734件から451件になりました。コンパイル時間の前後は記事#1129の表にまとめます。

もう一つは、ブリッタが独立したことで内部の整理がしやすくなったことで、矩形4種のループを1組に統合しました。画素ごとの書き込み値だけが違うので、その部分だけをcaseに閉じ込めました。

action
   case (b.op)
      BLIT_COPY:  vd <= romdata;
      BLIT_OR:    vd <= vdata | romdata;
      BLIT_ANDN:  vd <= vdata & ~romdata;
      BLIT_ERASE: vd <= 0;
   endcase
   xs <= xs + 1;
   xd <= xd + 1;
endaction

ブリッタ部分の規則数は8/3版の37から教科書版の14に減りました。規則数は、生成Verilogの中でWILL_FIRE_RLの信号として現れる規則の数です。8/3版はmkGameFSMの中のgfx_fsmに属する規則、教科書版はmkBlitter全体の規則を数えました。


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

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は増減なしです。定数の並べ方は静的展開の前にしか存在せず、展開後の回路には残りません。


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

posted by sakurai on August 10, 2026 #1103

JLCPCBのGerber Viewerによる図です。PCBを5枚だけでなく、PCBAも2枚依頼したので、以下は部品も実装した図となっています。

図1103.1
図1103.1 Gerber Viewer

製造はいつものようにJLCPCBに依頼し、待ちが0.8日間、PCBは0.8日間(5枚で2 USD)、PCBAは1.1日間(2枚で21.6 USD)、配送は2.9日(8.92 USD)でした。 最終的に8月4日夜に発注して、8月10日にPCBを香港からの航空便で受け取れたので、オーダーから5.7日(32.52 USD)という感じでした。23.7 × 24.7 mmのPCBです。

図1103.2
図1103.2 完成PCB

図1103.3に、2ch(L-ch/R-ch)がモード1で交互にPWM動作をしている図を示します。光の強さが交互に増減するモードです。

図1103.3
図1103.3 PWM動作


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

posted by sakurai on August 8, 2026 #1102

Geminiに設計してもらったプログラムをClaudeに修正してもらったものを以下に示します。

/* ============================================================
 *  NY_night  --  PIC12F1501 LED driver
 *  rev.13: mode 3 changed from simultaneous to L/R alternating blink
 *
 *  RA2 = PWM1 -> R2 -> T2 gate -> L_N   (Left channel)
 *  RA4 = PWM3 -> R1 -> T1 gate -> R_N   (Right channel)
 *  RA5 = mode button (SW1, to GND, internal pull-up)
 *  RA3 = unused input, brought out to JP1 pin 1 (MCLR/VPP)
 *
 *  Modes: 1 alternating fade -> 2 steady -> 3 alt-blink -> SLEEP -> 1 ...
 *  In sleep the core is stopped; a button press wakes it via IOC.
 * ============================================================ */

#pragma config FOSC     = INTOSC   /* internal oscillator                 */
#pragma config WDTE     = OFF      /* watchdog off                        */
#pragma config PWRTE    = ON       /* power-up timer on                   */
#pragma config MCLRE    = OFF      /* pin 4 is RA3, not MCLR              */
#pragma config CP       = OFF      /* code protect off                    */
#pragma config BOREN    = NSLEEP   /* BOR active when running, off asleep */
#pragma config CLKOUTEN = OFF      /* no clock output                     */
#pragma config BORV     = LO       /* low BOR threshold (battery)         */
#pragma config LPBOR    = OFF      /* low-power BOR off                   */
#pragma config LVP      = OFF      /* HV programming via JP1              */

#include 
#include 

#define _XTAL_FREQ 16000000

/* ---- timing, all expressed in 10 ms ticks ---- */
#define TICK_MS         10
#define FADE_TICKS       7     /*  70 ms per fade step (30 steps = 2.1 s) */
#define BLINK_TICKS     30     /* 300 ms per blink phase */
#define DEBOUNCE_TICKS   3     /*  30 ms settle time     */

#define DUTY_LIMIT     150     /* global duty cap, all modes.
                                * LED module has no current limiting of its
                                * own (measured 240 mA and rising at 5 V,
                                * duty 255).  Average LED current scales
                                * linearly:  I = 240 mA x DUTY_LIMIT / 255. */
#define HOLD_TICKS      60     /* 600 ms dwell at each end (trapezoid) */

/* Perceptual (gamma 2.0) ramp: duty = DUTY_LIMIT * (i / FADE_STEPS)^2.
 * Human brightness perception is roughly logarithmic, so a linear duty
 * ramp looks flat at the top and collapses abruptly near zero.  The
 * squared table spends most of its steps in the low-duty region where
 * the eye actually sees the change.  Regenerate if DUTY_LIMIT changes. */
#define FADE_STEPS      30
static const uint8_t fade_tab[FADE_STEPS + 1] = {
      0,   0,   1,   2,   3,   4,   6,   8,
     11,  14,  17,  20,  24,  28,  33,  38,
     43,  48,  54,  60,  67,  73,  81,  88,
     96, 104, 113, 122, 131, 140, 150
};

/* ---- PWM frequency exploration ----------------------------------
 * The module's flicker-IC white LEDs are progressively suppressed as
 * the PWM frequency rises (3.9 kHz: strong ~10 Hz flicker,
 * 15.6 kHz: mostly suppressed).  To let the flicker live, choose the
 * frequency with the Timer2 prescaler alone; clock and all tick
 * timing stay untouched.  PR2 = 255 in every case.
 *
 *   T2CON_RUN  prescale   PWM freq
 *     0x04       1:1      15.625 kHz   (= rev.9)
 *     0x05       1:4       3.906 kHz   (= original board feel)
 *     0x06       1:16        977 Hz
 *     0x07       1:64        244 Hz    (floor; below this the PWM
 *                                       itself becomes visible)
 * ---------------------------------------------------------------- */
#define T2CON_RUN 0x04
#define PR2_VAL 255
#define SET_L(v) (PWM1DCH = (uint8_t)(v))   /* duty ratio = v/256 */
#define SET_R(v) (PWM3DCH = (uint8_t)(v))

#define SW_PRESSED  (RA5 == 0)

/* ------------------------------------------------------------
 *  Output stage on / off
 *  With the PWM modules disabled the pins revert to port
 *  control; LATA = 0 holds both gates low, and R3/R4 back
 *  that up so the MOSFETs cannot float on.
 * ---------------------------------------------------------- */
static void outputs_off(void)
{
    SET_L(0);
    SET_R(0);
    PWM1DCL = 0;
    PWM3DCL = 0;
    PWM1CON = 0x00;
    PWM3CON = 0x00;
    T2CONbits.TMR2ON = 0;
    LATA    = 0x00;
}

static void outputs_on(void)
{
    LATA    = 0x00;
    SET_L(0);
    SET_R(0);
    PWM1DCL = 0;               /* POR value undefined -> would leave
                                * ~0.3% duty even at DCH = 0          */
    PWM3DCL = 0;
    PR2     = PR2_VAL;
    T2CON   = T2CON_RUN;       /* prescale select + TMR2ON            */
    PWM1CON = 0xC0;
    PWM3CON = 0xC0;
}

/* ------------------------------------------------------------
 *  Sleep until the button is pressed.
 *  GIE stays 0, so the core resumes at the instruction after
 *  SLEEP() instead of vectoring to an ISR -- no ISR needed.
 * ---------------------------------------------------------- */
static void sleep_until_button(void)
{
    outputs_off();

    IOCAF = 0x00;                  /* clear any stale edge flag  */
    IOCANbits.IOCAN5 = 1;          /* wake on RA5 falling edge   */
    INTCONbits.IOCIE = 1;
    INTCONbits.GIE   = 0;

    SLEEP();
    NOP();

    IOCANbits.IOCAN5 = 0;
    INTCONbits.IOCIE = 0;
    IOCAF = 0x00;

    outputs_on();
}

int main(void)
{
    /* ---------------- initialisation ---------------- */
    OSCCON = 0x7A;             /* 16 MHz internal -> PWM 15.6 kHz.
                                * Raised from 4 MHz / 3.9 kHz: a white
                                * LED in the module flickers ~10 Hz
                                * irregularly, suspected self-flasher IC
                                * resetting on the 3.9 kHz chopped supply */
    ANSELA = 0x00;             /* all pins digital                      */
    LATA   = 0x00;             /* outputs low before they are enabled   */
    TRISA  = 0b00101000;       /* RA5 = button in, RA3 = input only     */
    WPUA   = 0b00101000;       /* pull up RA5 AND RA3                   */
    OPTION_REGbits.nWPUEN = 0; /* enable weak pull-ups globally         */

    outputs_on();              /* PWM up, both channels at duty 0       */

    /* ---------------- state ---------------- */
    uint8_t mode     = 1;      /* 0:sleep  1:fade  2:steady  3:blink */
    uint8_t fi       = 0;      /* index into fade_tab, 0..FADE_STEPS  */
    int8_t  step     = 1;      /* ramp direction, +1 / -1             */

    uint8_t fade_t   = 0;
    uint8_t hold_t   = 0;      /* >0: dwelling at an end of the ramp */
    uint8_t blink_t  = 0;
    uint8_t blink_on = 1;

    uint8_t sw_raw   = 0;      /* 1 = pressed */
    uint8_t sw_prev  = 0;
    uint8_t sw_state = 0;      /* debounced   */
    uint8_t dbnc     = DEBOUNCE_TICKS;

    while (1) {
        /* -------- button: non-blocking, acts on release -------- */
        sw_raw = SW_PRESSED ? 1 : 0;

        if (sw_raw != sw_prev) {
            dbnc = 0;                       /* level changed, restart timer */
        } else if (dbnc < DEBOUNCE_TICKS) {
            if (++dbnc == DEBOUNCE_TICKS) { /* level has settled            */
                if (sw_state == 1 && sw_raw == 0) {
                    mode = (uint8_t)((mode + 1) & 0x03);
                    fi       = 0;
                    step     = 1;
                    fade_t   = 0;
                    hold_t   = 0;
                    blink_t  = 0;
                    blink_on = 1;
                }
                sw_state = sw_raw;
            }
        }
        sw_prev = sw_raw;

        /* -------- mode output -------- */
        switch (mode) {

        case 0:                             /* off -> sleep */
            sleep_until_button();

            /* Woken by a press. Go straight to mode 1 so that even a
             * very short tap responds, then swallow this press so the
             * release does not advance the mode a second time.        */
            mode = 1;
            while (SW_PRESSED) {
                __delay_ms(TICK_MS);
            }
            __delay_ms(30);                 /* release bounce */

            sw_raw = sw_prev = sw_state = 0;
            dbnc     = DEBOUNCE_TICKS;
            fi       = 0;
            step     = 1;
            fade_t   = 0;
            hold_t   = 0;
            blink_t  = 0;
            blink_on = 1;
            break;

        case 1:                             /* alternating fade, trapezoid,
                                             * perceptual (gamma) ramp      */
            if (hold_t > 0) {
                hold_t--;                   /* dwelling at an end: L off with
                                             * R full on, or the reverse     */
            } else if (++fade_t >= FADE_TICKS) {
                fade_t = 0;
                if (step > 0) {
                    if (fi < FADE_STEPS) {
                        fi++;
                        if (fi >= FADE_STEPS) hold_t = HOLD_TICKS;
                    } else {
                        step = -1;
                    }
                } else {
                    if (fi > 0) {
                        fi--;
                        if (fi == 0) hold_t = HOLD_TICKS;
                    } else {
                        step = 1;
                    }
                }
            }
            /* both channels follow the same perceptual curve, mirrored */
            SET_L(fade_tab[fi]);
            SET_R(fade_tab[FADE_STEPS - fi]);
            break;

        case 2:                             /* steady on */
            SET_L(DUTY_LIMIT);
            SET_R(DUTY_LIMIT);
            break;

        case 3:                             /* alternating blink: L and R
                                             * swap every BLINK_TICKS       */
            if (++blink_t >= BLINK_TICKS) {
                blink_t  = 0;
                blink_on ^= 1;
            }
            if (blink_on) { SET_L(DUTY_LIMIT); SET_R(0);          }
            else          { SET_L(0);          SET_R(DUTY_LIMIT); }
            break;
        }

        __delay_ms(TICK_MS);
    }
}

NY_night ファームウェア設計概要(内部設計)

PIC12F1501 / XC8、rev.13(最終版)

モード遷移・操作方法・書込みインタフェースなどの外部仕様は spec.md(NY_night LED コントローラ 外部仕様)を参照。 本書はファームウェアの内部構造と実装判断のみを記す。


ステートマシン

10 ms ティックの単一ループ。ブロッキング待ちを排し、フェード・点滅・ボタン判定を すべてティックカウンタで進める。旧版(Gemini 版)にあった __delay_ms(300) や while(RA5 == 0); による CPU 占有を廃した。 これによりボタン応答は全モードで 10 ms 以内。

消灯 = SLEEP(消費電流削減)

消灯中は CPU を停止し、待機電流を µA オーダーに落とす。 RA5 の状態変化割り込み(IOC)で復帰するが、GIE = 0 のままにして ISR を書かない (割り込み許可だけ立てておけば SLEEP から復帰でき、割り込みベクタへ飛ばず SLEEP 命令の次から実行が続く仕様を利用)。

SLEEP 前に PWM モジュールを止め、LATA = 0 でゲートを直接 Low に落とす。 基板側の R3 / R4(100 kΩ プルダウン)が二重の保険となり、 ハイインピーダンス期間中も MOSFET が誤オンしない。

BOREN = NSLEEP により、ブラウンアウト検出は動作中のみ有効・SLEEP 中は無効。 BOR 回路自身の消費(数 µA)を待機時から除く。

ボタン処理

  • 毎ティック非同期サンプリング + 30 ms デバウンス(3 ティック連続一致で確定)。
  • モード遷移は離しエッジで確定し、押しっぱなしでの連続遷移を防ぐ。
  • SLEEP 復帰時は「復帰したこと」自体をモード 1 入りのトリガとし、 その押下は飲み込む。短いタップでも確実に反応し、離しエッジで 二重にモードが進むことも防ぐ。

台形フェードの実装

ランプが上限・下限に到達した瞬間にホールドカウンタ hold_t を仕込み、 それが尽きるまでフェード処理をスキップする。ホールド中もボタン判定は 毎ティック回っており、応答性は変わらない。 L と R は PWM_R = 255 - duty の逆位相なので、L が下限ホールドの間 R は上限ホールドになる。

ピン設定の根拠

ピン 役割 設定
RA2 PWM1 → T2 ゲート → L_N 出力
RA4 PWM3 → T1 ゲート → R_N 出力
RA5 モードボタン 入力、内蔵プルアップ
RA3 未使用(JP1 の MCLR/VPP に接続) 入力、内蔵プルアップで固定

RA3 のプルアップは新基板で必須。旧基板と異なり RA3 は未実装スルーホールに しか繋がらず浮くため、固定しないと CMOS 入力の貫通電流が生じる (WPUA = 0b00101000)。

PWM は Timer2 / PR2 = 255、分解能 8 bit(デューティ比 = v/256)。 周波数は Timer2 プリスケーラ 1 箇所(#define T2CON_RUN)で選択でき、 0x04=15.6 kHz / 0x05=3.9 kHz / 0x06=977 Hz / 0x07=244 Hz。 最終値は 0x04(15.6 kHz)。

経緯: モジュール内の白 LED 数個が PWM 駆動下で約 10 Hz の不規則な チラつきを示した。DC 直結テスト(dctest rev.2)で「非常にちらつく」 ことが確認され、フリッカー IC 内蔵 LED の本来動作と確定。PWM 駆動 (平均 59%・断続給電)では IC が電力不足で本来のフリッカーを出せず、 周波数によって誤動作の出方が変わっていただけだった。全開のフリッカー は不要との判断で、最も抑制される 15.6 kHz を最終仕様とした。

調整パラメータ

ソース冒頭の #define で変更する。時間系はすべて 10 ms 単位。

名称 既定値 意味
FADE_TICKS 4 フェード 1 段の間隔(40 ms)
HOLD_TICKS 60 台形の両端ホールド(600 ms、最大 255)
BLINK_TICKS 30 交互点滅の切替間隔(300 ms)
DEBOUNCE_TICKS 3 デバウンス確定時間(30 ms)
FADE_STEP 5 フェード 1 段のデューティ増分
DUTY_MAX / DUTY_MIN 250 / 5 ランプの上下限

モード 3(交互点滅)の実装

rev.13 で同時点滅から L/R 交互に変更。blink_on のトグルで L=DUTY_LIMIT/R=0 と L=0/R=DUTY_LIMIT を入れ替えるだけで、 常にどちらかが点灯しているため暗転しない。

ビルド

  • XC8 Free モードで Flash 約 30% / RAM 約 27%。PRO ライセンス不要。
  • コンフィグは BOREN = NSLEEP / BORV = LO / LPBOR = OFF を含む (XC8 v2.x で確認済み)。

既知の留意点

  • LED モジュールに電流制限が無いことが実測で確定した (5 V・デューティ 255 で 240 mA、温度上昇に伴う増加傾向)。 rev.5 で全モード共通のデューティ上限 DUTY_LIMIT(既定 150)を導入し、 平均電流を約 140 mA に制限。フェードの上限 DUTY_MAX は DUTY_LIMIT - FADE_STEP として連動し、逆位相側も DUTY_LIMIT - duty で生成。 平均電流は 240 mA x DUTY_LIMIT / 255 で見積もれる。

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

posted by sakurai on August 7, 2026 #1101

Studio 500 3D Pop Up Wall Artの修理の続きです。前回のボードは基本的に動作したので、SMD化の改版を行います。改版はAIに手伝ってもらいます。まず製品の外観です。

図1101.1
図1101.1 Studio 500

次にAIに改版してもらった論理図です。部品はSMDタイプを選択させました。電子スイッチは2SCを使用していましたが、CMOSに変更しています。

プルダウン抵抗を100kΩにしたのは、駆動時の無駄電流を抑えるためです。RA2/RA4がHighのとき、470Ωを通してゲートに5Vがかかり、100kΩ経由でGNDへ50µAが流れ続けます。2チャンネルで100µA。単4電池駆動なので、ここは小さいほうが望ましい。

一方でリーク電流に対する引き込み能力としては100kΩで十分です。AO3400Aのゲートリークは最大100nA程度なので、100kΩなら10µVしか持ち上がりません。

なお470Ω(R1/R2)ゲート容量への突入電流を制限してPICのポートを保護し、スイッチングのリンギングを抑えます。100kΩとの分圧比は99.5%なので、オン時のゲート電圧はほぼVDDそのままです。

なおAIの部品配置は配線が省略され適当なので、部品間の配線は人手で接続しています。

図1101.2
図1101.2 点滅回路図

次にレイアウト図を示します。人手で部品を配置し、基本的にはEAGLEで自動配線しました。ただし電流が大きいところは太さを変えています。

出来上がりをClaudeにみせたところ、背面GNDと前面GNDの間のVIAが少なく、前面GNDパッド位置でVIAを打ち、背面の大きなGNDプレーンにコンタクトすべきとのことで、VIAを追加しました。今回はSMDなので、前面にVDDとGNDが混在するように変更されたためです。

PCBサイズは23.7 × 24.7 mmとなりました。SMD化により旧ボードと比べて▲53%となっています。

図1101.3
図1101.3 点滅回路レイアウト図


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

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

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


ページ: