こんにちは、パレイド技術部の夏目です。前回の「V3 対応(1)」では、Family BASIC を自動で叩いて挙動を観察するハーネスを、Family BASIC V3 の ROM でも回るようにした話を書きました。今回はその続きで、V3 固有命令の観察を始めた最初の一歩でつまずいた記録です。つまずいた命令は FILTER。画面全体を着色するという命令なのですが、改造した nesemu(TypeScript 製の NES エミュレータ)で実行しても、画面の色がまったく変わりませんでした。
本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。
結論から書くと、色が変わらなかった原因は ROM 側ではなく、エミュレータ側の未実装でした。FILTER が使う「色強調(color emphasis)」という PPU のハードウェア機能を、nesemu が完全に無視していたのです。PPU に手を入れて実装したら見えるようになり、ついでに公開資料の表が間違っていることまで分かりました。その顛末を、数字と画像で記録します。なお検証に使った ROM は実機のカートリッジを私的に購入して自分で吸い出したもの(著作権法 30 条の私的複製)で、ROM ファイル自体の配布はしていません。
通る、けれど見えない
FILTER は V3 で増えた命令で、FILTER 1 のようにオペランドを 1 つ取り、画面全体を着色します。ところが改造 nesemu で FILTER 1 を実行しても、画面の色は変わりませんでした。困るのは、命令自体は通ることです。?SN ERROR(シンタックスエラー)のような反応は一切なく、エミュレータは何事もなく次のプロンプトに戻ります。命令が無視されているのか、効いているけれど見えないのか、画面を見ているだけでは判断がつきません。
原因を ROM 側とエミュ側のどちらに絞るか、2 ステップで切り分けました。
- 改造 nesemu で
FILTER 1→ 画面の色は変わらない - 別のエミュレータで同じ ROM・同じ
FILTER 1→ 画面の色が変わる
同じ ROM で挙動が割れたので、問題は ROM 側ではなく nesemu 側だと確定できます。FILTER の解釈そのものは ROM が正しく行っていて、その結果を画面に反映する段で落ちている、という見立てです。
正体は、英語の nesdev フォーラムと国内の moainet.org で分かりました。FILTER はオペランド 0〜7 で、PPU の「色強調ビット(color emphasis bits)」を立てる命令でした。PPU(Picture Processing Unit、NES の映像生成チップ)には PPUMASK($2001、表示設定をまとめたレジスタ)があり、そのビット 5〜7 が色強調ビットです。色強調とは、特定のチャンネル(赤・緑・青)を強調する——正確には、それ以外のチャンネルを淡く減衰させる——ことで画面全体を着色するハードウェア機能で、パレット(色そのものの定義)は一切変えません。FILTER はこのビットを BASIC から叩くための薄いラッパーでした。
では、なぜ nesemu で見えなかったのか。内部を見ると、各画素を実際の RGB 値に変換して画面に出す描画処理が PPUMASK のうち GREYSCALE(bit0、白黒化)しか見ておらず、色強調ビットを完全に無視していました。そもそも実装上の型定義に色強調ビットそのものが存在せず、見ていないどころか語彙として持っていません。これでは FILTER が PPUMASK のビットを正しく立てても、画面には何も起きません。無視されていた理由は 2 つあります。
- アーキテクチャの都合: nesemu の描画は「パレット番号 → 固定色テーブルを直接引く」だけの構造です。GREYSCALE はパレット番号をマスクするだけで済むので安く実装できますが、色強調は引いてきた出力 RGB を実際に減衰させる計算が必要で、固定表を引いて終わりという構造と相性が悪い。表の手前ではなく後ろに処理を足さないと表現できません。
- 優先度: 色強調を実用する NES ゲームは全画面フェードなどごく一部で、一般ゲームを動かす目的では実装の優先度が最下位です。上流の nesemu(tyfkda 氏)も edge feature(端の機能)として省いていました。妥当な判断です。そして Family BASIC V3 の FILTER は、その数少ない「色強調を実用するケース」のひとつでした。一般ゲーム用エミュレータが切り落とした端の機能を、1984 年の BASIC 命令がちょうど踏みにいった格好です。
ここで方針を 1 つ正直に書いておきます。これまでこの連載では、エミュレーションコアには手を入れず、周辺機器の制御層に閉じる方針で改造を進めてきました。今回はその方針を一点だけ破っています。色強調は周辺機能でどうにかなるものではなく、PPU そのものの未実装機能だったので、例外的にコアの PPU に手を入れました。
実装と検証
実装は、色強調をフレーム末のポストパス(1 フレーム描き終えた後に画素バッファを一度だけなめる後処理)として足しました。既存の GREYSCALE がフレーム単位で一定として扱われているので、それに合わせています。手を入れたのは、PPU の型定義に色強調ビットを足すことと、描画処理の末尾で色強調の後処理を呼ぶことの 2 点です。
| 変更箇所 | 変更内容 |
|---|---|
| PPU の型定義 | PPU マスクレジスタに赤・緑・青の色強調ビット定義を追加 |
| 描画処理の末尾 | フレーム末で色強調の後処理を呼ぶ |
各チャンネルは「自分以外の強調ビットが立っている数」だけ累積で 0.75 倍に減衰します。赤を強調するなら緑と青が落ち、相対的に赤が際立つ仕組みです。強調ビットが 1 つも立っていなければ即 return するので、色強調を使わない通常ゲームへの影響はゼロです。減衰係数の部分を抜き出すと、こうなります。
// mask = PPUMASK の現在値。各強調ビットが立っているかを 0/1 にする
const r = (mask & EMPHASIZE_RED) ? 1 : 0
const g = (mask & EMPHASIZE_GREEN) ? 1 : 0
const b = (mask & EMPHASIZE_BLUE) ? 1 : 0
// 各チャンネルは「自分以外」の強調ビットの数だけ 0.75 倍に減衰
const rMul = 0.75 ** (g + b) // 赤は緑・青の強調ぶん暗くなる
const gMul = 0.75 ** (r + b)
const bMul = 0.75 ** (r + g)
// pixels[i] *= rMul, pixels[i+1] *= gMul, pixels[i+2] *= bMul
検証は、Node.js で動く自動実行スクリプトに PNG スクリーンショット機能を追加して行いました。FILTER 実行後の画面を撮り、白文字の画素値を読みます。Family BASIC の白文字は実装上 (253, 253, 253) なので、ここが色強調でどう変わるかを見れば数字で確かめられます。
実装前は、FILTER を実行しても画面は素のままでした。

実装後に FILTER 1 と FILTER 4 を撮ると、こうなりました。


| FILTER | 強調チャンネル | 白文字の RGB | 見た目 | 判定 |
|---|---|---|---|---|
| (なし) | なし | (253, 253, 253) | 白 | ○ 基準 |
| 1 | 赤 | (253, 189, 189) | 赤 / ピンク | ○ 一致 |
| 4 | 青 | (189, 189, 253) | 青 / ラベンダー | ○ 一致 |
減衰させたチャンネルは 253 × 0.75 ≈ 189。赤強調で (253, 189, 189) になるのは計算と合う。
数値も目視も一致しました。あわせて、色強調の性質どおり画面の黒い部分には着色が効かない(黒 × 係数 = 黒)ことも確認できました。着色が出るのは白文字のような明るい画素だけで、これは実機の色強調の挙動そのものです。背景が黒い Family BASIC の画面では、文字だけが色づいて見えます。
おまけ
検証中に小さな逆転がありました。FILTER のオペランドと色の対応が、公開資料(moainet.org)の表と食い違うのです。moainet は「3 = 青、4 = 黄」としていますが、実機 ROM を実装後のエミュで撮ると 3 = 黄(赤+緑)、4 = 青でした。

実機が正しいとして辻褄を合わせると、FILTER のオペランドは単純な 2 進の強調ビットマスクだと分かります。bit0 = 赤(1)、bit1 = 緑(2)、bit2 = 青(4)で、足し合わせると 3 = 赤+緑 = 黄、5 = 赤+青 = 紫、6 = 緑+青 = 水色、7 = 全部 = 減光グレー。きれいなビット演算で、覚えやすい体系です。公開資料の表のほうが誤っていて、エミュに実装したことで逆に正しい対応を reverse-engineer(挙動から仕様を逆算)できたことになります。資料を信じて実装していたら、3 と 4 を取り違えたまま「資料どおりに動いた」と勘違いしていたはずで、自分で撮った数字を信じる価値を改めて感じました。
まとめと、次回に続く
PPU の色強調を実装して、FILTER が見えるようになりました。
次回からは本題の V3 固有命令の観察に戻ります。色強調のように「エミュ側が対応しないと観察できない」命令が他にもないかを洗いながら、V3 コーパスを観察し直します。その先には、揃えた V3 コーパスを使った LoRA 対応(実機観察のデータをモデルの重みに焼く方向)も控えています。