ベンチを運動させる
2026-09-17、ヘッドの手が初めて効いた晩に書いた: Pi からのリセットと電源、バイトを
コンソールに通して一つ残らず読み返すブリッジ、スコープを止めるトリガ、その全部が
ワークステーションからの tools/bench-check.py で二度とも緑になった。それが v1 の
ベンチだ。この文書はそれを運動させるための計画だ: 一段ずつ難しくなっていく課程
(regime) で、どの段も同じ四つのもの (台本、模型の走行、比較器、ゲート) からでき、
どの段も、自動化がこれから書かれることになる言語に単語を一つ足す。課程の上には
三つの取り組みが乗っていて、それぞれ以下に自分の節を持つ: 実機に合わせて調整する
模型、パッドから絵まで学び取るゲーム、そしてゲームのコードの X 線だ。
二つのスタックと、その間を動くもの
上のシートが論理の図で、回路図の横で tools/draw-schematics.py が描き、
tools/check-sheets.py がコミット済みの写しに対して押さえている。上から下へ四つの
帯:
- 駆動側。 台本一つ (
script.md) を、両側が読む。ワークステーションの クライアントがそれをヘッドに渡し、同じファイルの上で模型自身のランナーを走らせる。 回帰テスト (カメラにはrig-check.py、電子にはbench-check.py) はその横に 座っていて、どの段も信じられる前に緑になっている。 - 実機。 改造していない NES-001。ヘッドは LAN 上の Raspberry Pi で、シリアル ポートにブリッジ、GPIO に二本の手、SCPI にスコープ、USB にグラバー (映像の取り 込み装置) とサウンドカードが付く。ブリッジはコンソールのコントローラポートを 握り、手はフロントパネルを握り、スコープとグラバーは出力を握る。目はベンチ自身を 見ていて、絵は見ていない。
- 模型。 Rust のクレート: コードとしてのコンソール、信号経路、ファイルとして 書き出される走行、そして、どの高速ラングもそれに対して押さえられるスイッチ レベルのチップ。実機と同じバイト列 (リーダーの吸い出し、CRC で照合) と、同じ 予定表 (ラッチ番号ごとのバイト)。
- 比較器。 それぞれ記録だけを読み、名前の付いた行一つで答えるか、報告すること 無しと答える。ゲートは緑か、その行を名指しするかで、MUTATE はそれを赤にしなけれ ばならない。
どの矢印にも運ぶものの型が付いていて、物が動く向きを指している:
| 型 | 運ぶもの | どこへ行けるか |
|---|---|---|
| コマンド | 台本の語、ヘッドの op、ブリッジの行、GPIO のレベル、SCPI | 下へ。台本から実機または模型に向かって |
| 刺激 | あるラッチ番号でのパッドのバイト、コンソールの唯一の入力 | コンソールのポートへ、または set_pad へ |
| 信号 | アナログの映像と音声 | 実機から出るだけ。模型に線は無い |
| 記録 | ログ、捕捉、トレース、判定 | 上へ。走行ディレクトリに入り、それから比較器へ。決して下へは行かない |
| 絵 | フレーム一枚 | 採点へ、または窓へ |
| オラクル | 半サイクルごとに押さえる | 二つのものの間の比較であって、流れではない |
| 提案 | 単語が存在するように描いてあるだけ | まだどこへも |
シート全体が守る規則が二つある。制御は台本から下へ流れ、比較器から上へ出ること
は決して無い。 比較器は行一つで答える。それが言うことは、台本かノブを人が
編集するのを通してしか、日付とそれを正当化した走行と一緒にしか、コンソールにも
模型にも届かない。それが、当てはめた模型が自分自身に当てはまってしまうのを防いで
いる。一つの単語は両側で同じことを意味するか、でなければ自分の側を名乗る。
SET と AT は実機でも模型でも成り立つ。ARM と CAPTURE はヘッドのもので、
模型のランナーはそれを名指しで飛ばす。set_pad は模型のもので、実機にそんな動詞は
無い。両側が守るか名指しで拒むかのどちらもできない単語は、この方言に入らない。
今日の方言、層ごとに
自動化の土台は、課程が教える単語で書かれることになる。だから単語は入ってきた順に 記録する。これが今あるもので、誰が守るかも一緒に:
| 層 | 単語 | 誰が守るか |
|---|---|---|
| 配線 | ブリッジの行 MODE PASS、MODE INJECT、SET hh、AT n hh、TRIG n、RESET、STATUS、GPIO のレベル、SCPI | UNO のファームウェア、Pi の pinctrl、スコープ |
| ヘッド | UDP JSON 越しの op status、run、abort、bridge、runs、そして 2026-09-21 からは pad、HTTP 越しの runs/<stamp>/ | head/headd.py |
| 台本 | RESET、POWER ON、POWER OFF、MODE、SET、AT、TRIG、ARM、CAPTURE、WAIT | ヘッドはこれら全部を演奏する。模型のランナーは SET と AT をラッチ番号で守り、残りは名指しで飛ばす |
| 模型 | set_pad、run_frames、master_half_step、cpu_trace、Alignment、ランナー pad-log、trace、capture-score、split-score、run-rom | nes-console |
| 比較器 | compare-logs.py、b1-score.py、split-score.py、eyes.py、cal.py、replay-recorded | ワークステーション |
| 回帰テスト | rig-check.py と bench-check.py。どちらも検査の名前ごとに PASS、FAIL、SKIP で答える | ワークステーション、手については Pi |
台本より一つ上の層こそ、この課程が建てるものであり、そこにはまだ単語が無い。 それが必要とする名詞を、ここで提案しておく。以下の各段で一つずつ入る: step (台本、模型の走行、比較器、ゲートの四つ組に名前を付けたもの)、 run (一つの step を一回演奏したもの、スタンプ付き)、 latch (両側が数える番号)、frame (あるラッチでの絵)、 window (ダイのページが立てる、走行の一区間)、 screen (名前の付いたフレームの類)、episode (始まりから終わりまでの台本)、 knob (出自を伴う、模型の名前付きパラメータ)、 pattern (ゲームのコードの中の、名前の付いた形)。動詞は: exercise (一つの step を演奏する)、compare、replay、bisect、 fit、reach、xray。どれもある段がそれを守るまでは提案のままで、この 一覧に無い単語を必要とする段は、その段と一緒にここへ足す。
課程、一段ずつ
どの段も同じ四つのもので、どれも走る前に述べたゲートで閉じる。bench-plan.md が
B0 から B3 とそのゲートを述べており、ここで変えるものは何も無い。以下の段は、
それらに順序どおり到達する道筋で、それぞれが方言に足すものも書いてある。
| 段 | 何が走るか | ゲート | MUTATE | 足す単語 |
|---|---|---|---|---|
| E0、今日成立している | rig-check.py、それからコンソールのスイッチを切って bench-check.py --hands head、入れて --hands manual | どの検査も PASS。二本の手がポーリングの流れを止める。ウォークがすべてのバイトを読む | 検査自身のもの: 古びた基準、入れ替えたバイト | step、run: 回帰テストが最初の段で、その出力が最初の走行だ |
| E1、一巡 | 較正カートリッジの上の閉じた一巡 (closed-cycle-plan.md): 電源、リセット、カラーバーの画面、TRIG、捕捉の復号、同じラッチでの模型のフレーム | B1 のカラーバーのゲート: N6 の領域と許容を、終端した実機で | 模型の側だけ台本を 1 ラッチずらす | frame、compare |
| E2、ゲーム一つ | 台本でゲームのタイトルに到達する: MODE INJECT、AT n のバイトでタイトルを抜けて入力に依存する最初の画面へ、そこでトリガ、b1-score.py | 最初に落ちた領域を名指し、または落ちた領域無し | 同じずらし | episode |
| E3、手一つ | B3: MODE PASS でパッドの上の手を記録し、その記録を実機と模型で再生し、同じトリガ番号で捕捉する | 実機と実機が領域ごとに一致すること。模型と実機は、トリガ番号についての二分探索で最初に離れるラッチを名指しする | 1 バイト落とした再生 | replay、bisect |
| E4、掃引一つ | B2: 台本による電源投入 100 回、マスタクロックはスコープの空きチャンネルに。リセットのリレーが離れてから最初のフェッチまで。音声ジャックにミキサー ROM | アライメントのヒストグラムを印字。リセットの保持時間を実測。音声を模型の段に対して採点 | 分類器に 1 クロック周期ずらした捕捉を食わせる | knob、fit: 打ち込むのではなく実測から設定される最初のノブ |
| E5、無人 | 上の段を一覧から、毎晩、報告一つ | どの段の行も出ること。落ちた段は自分の行を名指しし、ほかの何も止めない | 一覧の中の一段をわざと壊す | exercise |
順序は、何が存在するかによって決まっている。E1 は較正カートリッジの実機側
(フラッシュチップ、cart-blanks.md) を待っているので、E0 の後に走らせる最初の段は
同じ仕掛けを使うゲームでの E2 になる。E3 は新しいものを何も必要としない。E4 は
スコープの空きチャンネルをマスタクロックに当てることを必要とし、それはプローブ
一本と一度の座り込みで済む。E5 は、各段が台本になってしまえば一覧と cron の項目
一行で、そしてここが土台の始まりだ: 段はファイル、走行はディレクトリ、報告はその
行の集まりになる。
E2、演奏した: 2026-09-18
E0 より先の最初の段は、課程を書いたその晩に二度走った。相手はマルチカートの
Super Mario Bros. のタイトルだ: exercise/e2-title.txt、これが上で言っている
ヘッドと模型が共有する台本で、MODE INJECT、SET 00、RESET、ラッチ 200 と 201
で Start (08)、スコープは CH1 で構え映像は CH3、TRIG 300、そして捕捉。図の
流れが走る順で:
- ヘッドがユニットになった。 Pi の上で
head/setup.sh --bridge /dev/ttyACM0 --baud 115200 --scope <ip>(ブリッジは 2026-09-24 から/dev/nes-bridgeだ):nes-bench-head.service、有効化済み。これはserial-bridge.serviceと衝突する (二つが一つのポートを開くと、Linux は二人の 読み手に何も言わずバイトを分け合わせる) し、止まるときは両方のピンを起動時の レベルに戻す。 - まず E0。
hands-head.sh --no-walk: ブリッジ、スコープ、ポーリング (20 秒で 1203 回、それぞれ 8 クロック)、トリガとリセット (GPIO17 からポーリングが 2.50 秒 止まった) が PASS。電源は FAIL、30 秒のあいだ止まらない。コンソールの前面 スイッチが入っていてリレーを迂回していた: これはリレーではなくスイッチについての 事実で、そのリレーは前日、スイッチを切った状態でコンソールを二度止めている。E2 に 必要な手はリセットだけなので、スイッチを入れたまま走らせた。その晩あとで、 スイッチを切って: リレーを開いた状態では 3 秒間ポーリングが無く、それからヘッドの 台本 (MODE PASS、RESET、POWER ON、WAIT 12 S、POWER OFF) が 713 回の ポーリングを記録し、オフの後コンソールはまた静かになった。二本の手はヘッドの下で 成り立つ。それが E1 に必要なことだ。 - 実機。 走行
20260918-000139: リセット、200 と 201 で Start、ラッチ 300 で トリガ、CH1 と CH3 で 50 MSa/s の 12 M ポイント、トリガは標本 5,000,000 の位置で その後ろに 140 ms の記録、スコープから読み出すのに 58 秒。同じ台本での ブリッジの記録とpad-logの突き合わせ: 315 ラッチ、バイトもクロックも一つ残らず 一致。 - 模型。
b1-score.pyはcapture-scoreをラッチ 300 の後の最初のフレームまで 走らせる: 模型のフレームには平らな領域が二つ、$17(タイトルの箱、行 40..68、 x 128..215) と$22(空、行 32..127、x 216..256)。トリガから復号した実機の フレーム: 復元 -4.1 ppm、バースト残差の最悪値 0.028、アンカー行 266。 - 二度目の走行、
20260918-000521。同じ台本で、映像を 1 目盛 500 mV ではなく 200 mV にした: 残差 0.009、アンカー行 268、領域は同じ。
| 領域 | 記録 | dY | dsat | dhue |
|---|---|---|---|---|
$17 行 40..68 | 500 mV/div | -0.039 | -0.045 | -3.0 deg |
$22 行 32..127 | 500 mV/div | -0.039 | -0.060 | -9.1 deg |
$17 行 40..68 | 200 mV/div | -0.054 | -0.062 | -3.1 deg |
$22 行 32..127 | 200 mV/div | -0.067 | -0.079 | -9.1 deg |
$17 行 40..68 | 200 mV/div、20260918-152924、冷間電源投入 + 温間リセット | -0.038 | -0.046 | -3.5 deg |
$22 行 32..127 | 200 mV/div、20260918-152924、冷間電源投入 + 温間リセット | -0.037 | -0.061 | -9.3 deg |
2026-09-18 午後、フレーム規則の下での再走行 (模型の絵は、垂直同期に対する
ラッチの位置から置かれる。mario-dissection.md の「The split」、nes @ efbcc46)。
検査は二つ。朝の 200 mV の記録を採点し直した: ラッチ 300 は PPU の行 249、ドット
65 に落ち、これは開始より後なので、模型の絵は 1 フレーム動いた。それでも上の二行の
数字はどれも千分の一の桁まで戻ってきた (静止した絵はフレームを見分けられない、
というのがその主張だ)。そして三つ目の記録、exercise/e2-title-warm.txt (コンソール
はリレーで冷間から電源投入、温間リセットの手順、同じ押しと同じトリガ。朝の
コンソールは 1 時間走り続けていた): それが三組目の行だ。色相はまた繰り返した。
三枚目の捕捉でも 0.4 度の内側で。輝度と彩度は朝の 200 mV の記録から 0.017 から
0.03 のところに落ち、これは下の 2 番で二つの目盛の違いに帰していた差と同じ大きさ
だ。だからあの帰属はそれだけでは立たない: 一つの目盛での記録二つがそれと同じだけ
違っていて、ほかに違うのはコンソールの温まり (1 時間走ったものと数秒のもの) と
記録のレート誤差 (-4.3 と -8.0 ppm) だ。当てはめではなく記録した。輝度のレベルが
自分の数字を得るのは、スコープだけで測る E1 のカラーバーのカートリッジだ。
輝度のばらつきは同じ日に答えが出た (2026-09-18 の晩): 採点器の量子化された
レベルと、コンソールの温まりだ。 暖機の系列 (tools/warmup.sh、
exercise/warmup-first.txt と warmup-step.txt): コンソールをオフから電源投入し、
すぐ E2 の押しと捕捉、それから電源を入れたまま 5 分ごとにもう一度、47 分にわたって
1 目盛 200 mV で捕捉 10 枚 (走行 194516 から 203018)。E2 と同じように採点すると、
領域の輝度は三枚のあいだ -0.034 の近くを保ち、698 秒から 1002 秒の間に -0.050 と
-0.061 へ段を踏み、そこに留まった。レート誤差は -4.3 から -3.1 ppm へ滑らかに
漂い、段は踏まなかった。記録そのものも段を踏んでいない: 消去期間の平均は段を
またいで 88.48、88.51 コードと登り、これは 1 コードの百分の一で、中央値は 88 から
89 へ動いた。ntsc-crt はレベルを中央値として読んでいて、u8 の記録の上では整数の
コードになり、しかもそこの同期は 22.5 コード分の深さしかない。だからポーチが半
コードを跨いだとき、あらゆる領域を採点する利得が 4.5% 動いた。ntsc-crt v0.2.11 は
レベルをトリム平均として読む (量子化した合成の記録で押さえてあり、中央値はそこで
赤になる)。それで採点し直すと段は消え、残ったのは滑らかな漂いで、$17 は電源投入
時の -0.039 からおよそ -0.048 へ、$22 は -0.042 からおよそ -0.055 へ、30 分ほど
から先は平らだ。色相はその間ずっと -3.1 度と -9.2 度のまま。つまりこれは
コンソールの温まりで、最初の 30 分で輝度の百分の一ほどだ (2026-09-19 からはノブに
なっている: 取り組み 1 の「温まり」)。E2 の記録を v0.2.11 で採点し直すと: 500 mV の
記録は -0.043 と -0.047、冷えた 200 mV の記録 (152924) は -0.042 と -0.045、朝の
1 時間温まった 200 mV の記録は -0.050 と -0.059 で、温まりきった平地の上にある。
だから下の 2 番は置き換えられた: レベルをコードとコードの間で読めば二つの目盛は
一致し、記録を分けていたのは推定量と温まりだった。上の表の行は、その時点で採点した
ままのものだ。
上の表が述べるゲート: 最初に落ちた領域を名指し。$17、行 40..68、どの軸でも。
そしてその後ろに $22。二つの記録が判定の先に言うこと:
- 色相は 0.1 度の桁まで繰り返す。 捕捉二枚、垂直方向の目盛二つにわたって:
模型の空は実機のそれより 9 度青く、茶色は 3 度暖かい。符号は同じで、大きさは
9 月にタイトルの上で二つの目が測ったもの (
eyes.py、12.6 度と 14.1 度) と だいたい同じだ。いまや三つの計器が、模型の絵の連鎖こそが異物だと一致している。 取り組み 1 の未決の項目 (符号化器のレベル依存の位相) には、押さえるべき実機の 数字ができた。 - 輝度と彩度はスコープの目盛と一緒に動く (同じ晩に置き換え、上記: 採点器の 量子化されたレベルだった)。動く量は 0.015 から 0.02 で、これは粗い記録の 1 レベル分だ: 1 目盛 500 mV では絵は 256 レベルのうち 43 を覆い、200 mV では 100 を覆った。ボルトで見れば二つの記録は一致する (同期の先端は 0 V 近く、消去期間 0.40 V、頂点 0.76 V、スコープのメガオームに入れて)。だから細かいほうの記録を 信じるべきで、台本はいま 200 mV で構える。そして粗い記録から復元が読み取る レベルは、それ自身がノブ一つ分の誤差だ。残っている外れ (輝度 -0.05 から -0.07、 彩度 -0.06 から -0.08) はここでは決着しない: 同じ空は 2026-09-02 の直接の捕捉 では彩度が 28 パーセント高く読めていて、一つのスコープを通した一つの実機の 読みが二つ、それだけ食い違うということは、プローブと結合と終端がその数字の内側に 入っているということだ。決めるのは E1 の終端したカラーバーの捕捉で、それは フラッシュチップを待っている。
- 道具が三つ、最初の実走行で壊れて、その晩に直った。 合成の走行での予習では
できなかったのがこれだ:
b1-score.pyは--channelを覚えるまでトリガの線を 採点していて (二チャンネルの捕捉のfile =の行は CH1 を名指しする)、いまは チャンネルの無い捕捉を名指しで拒む。同じ道具は模型に、間違ったチェックアウトから の相対パスを渡していた。bench.pyは、ヘッドがスコープを読むのに費やす 1 分の あいだ黙ったときに死んだ。 - 単語。 episode とは、一つの ROM の上で
RESETから最後の捕捉まで演奏した 一つの台本のことだ: 走行ディレクトリ、ROM の CRC で引く鍵、台本、そして捕捉。 episode が二つ演奏された。以降のどの段も、これで組み立てられる。
二つ目のゲーム、Duck Hunt の野原での E2: 2026-09-19
Super Mario Bros. のタイトルは採点器に平らな色を二つしかくれなかったので、色相の
外れは点が二つで曲線を持たず、輝度の隔たりは利得なのかオフセットなのか言えなかっ
た。マルチカートの二つ目のゲーム Duck Hunt には、大きな平らな色でできた野原が
ある。台本 exercise/dh-field.txt はまず模型の上で見つけた (capture-score に
SHOW=path.ppm が付き、選んだフレームを復号して出せるようになった): メニューは
ラッチ 200 で Select、240 で Start を取る。Duck Hunt のタイトルはラッチ 500 あたり
まで Start を無視し、600 から 10 ラッチ押し続けたものを取る。Game A の野原はラッチ
900 で捕捉する。二度演奏し (電源を入れて 24 秒での走行 20260919-183613 と、211
秒での 183919)、それから同じ温まりでもう一度 Super Mario Bros. のタイトルを
(184121、warmup-step.txt)。
採点器の領域は最大の長方形になっていなかった。 最初の模型の走行は野原で領域を
一つ見つけ、それは地面の帯で、空ではなかった。capture-score は行を貪欲に
併合していて、どの行も自分と重なる最初の連なりに従い、その交わりだけを保っていた
ので、木の葉が空全体を左端の 8 ドットの薄片まで切り詰めてしまった。領域はいま、
色ごとの「全部が一色の最大の長方形」(そのマスクの極大長方形) になっている。注釈が
最初からそう言っていたとおりだ (MUTATE_REGIONS=1 で古い併合が走る)。カラーバーの
カートリッジでは何も動かない: どの輝度の行でも 13 領域中 13、トリガを遅らせる妨害は
今も赤。Super Mario Bros. のタイトルでは古い二領域が輝度で 0.0002、色相で 0.1 度
動き、さらに二つ、下辺に沿った緑の $1a と $29 が現れる。
実機は両方の走行で野原に到達し、その絵は模型の画面だ (粗い形で模型に対して r 0.998、2 フレーム後の自分自身に対して 0.9997)。二つの輝度レベルに六つの色、温まりの ノブは入れて:
| 領域 | 輝度 | 色相 (模型) | dY | dsat | dhue |
|---|---|---|---|---|---|
$1a タイトル、行 200..208 | 0.35 | -118.5 | -0.036 | -0.049 | -0.2 |
$17 野原、行 166..173 | 0.33 | +150.1 | -0.043, -0.045 | -0.052, -0.054 | -3.5, -3.4 |
$17 タイトル、行 33..72 | 0.34 | +149.8 | -0.041 | -0.052 | -3.3 |
$18 野原、行 226..240 | 0.35 | -177.2 | -0.028, -0.031 | -0.046, -0.047 | -7.9, -7.9 |
$22 タイトル、行 23..192 | 0.65 | 0.0 | -0.045 | -0.068 | -9.2 |
$29 野原、行 168..181 | 0.65 | -149.9 | -0.045, -0.048 | -0.067, -0.069 | -10.5, -10.5 |
$29 タイトル、行 200..208 | 0.65 | -148.8 | -0.040 | -0.065 | -10.9 |
$21 野原、行 0..122 | 0.65 | -30.0 | -0.044, -0.047 | -0.067, -0.069 | -11.9, -11.9 |
八つの行が言うこと、当てはめではなく記録として:
- 色相の外れはレベルと一緒に大きくなる。 高い輝度の行の三色は -9.2 度から -11.9 度外れ、低い行の三色は -0.2 度から -7.9 度外れる。大半を担っているのは レベルだ (平均は -10.6 と -3.8)。行の中では色相が残りを担い、低い行ではその分が 多い。どの色相も走行、ゲーム、領域をまたいで 0.4 度の桁まで繰り返す。これは 実機の出力の中のレベル依存の位相の形であり (取り組み 1 の表にある未決の項目、 そして負荷の下での目の発見)、いまは実機の上に点が六つある。カラーバーの カートリッジはそれにあらゆるレベルとあらゆる色相を与え、定数はそこで当てはめる。
- 輝度の隔たりはオフセットで、利得ではない。 どちらのレベルもだいたい同じだけ 外れる (0.33 と 0.65 の上側の行で -0.041 から -0.048)。利得ならば高い行で 二倍外れるはずだ。彩度は逆だ: 実機のクロマは、どちらのレベルでも模型より 12 から 15 パーセント足りない。これは利得だ。
- 一番下の行は外れが小さい。 行 200 以下の領域は輝度で -0.028 から -0.040 と
読み、それより上のものは -0.041 から -0.048 と読む。
$29はそれを一色の上で 見せている (行 200..208 で -0.040、168..181 で -0.045 と -0.048)。野原を下に 向かって動くレベルならこうなる。フレームの下端近くの領域が、復元の行が尽きる ところに座っていてもこうなる。まだ分けられていない。2026-09-20 に下で答えた: これは行ではなく色のものだ。 - フレームが一つずれている。 実機の絵は、模型のフレーム F-1 と F+1 に対して
全解像度で (r 0.94) F (0.91) より良く合う。Super Mario Bros. の分割と E3 は F を
見つけていた。野原は今も領域があるところにあるので、色はそれに依存しない。
フレーム (あるいはそのドットクロールの位相) は
open-items.mdに記録してある。 位置合わせのずれは 4 標本、半ドットで、4 分の 1 ドットの項目と同じ向きで少し 大きい。2026-09-20 に下で答えた: フレームは F であり、読み取られていたのは色の 位相だった。
温まりのノブの秒数は、二度目の走行では最初に書いたとき間違っていた: 台本は
POWER ON で開くのに、リレーはすでに閉じていて、それでもヘッドはその語を記録する
ので、ノブは二度目の走行自身の始まりから数えてしまった。tools/knobs.py はいま、
最後の off の後の最初の on から数える (暖機の系列の秒数は動かない)。24 秒と
211 秒の間で、野原の輝度は曲線が取り除く分より 0.002 から 0.003 多く動いた。これは
曲線を当てはめた晩の最悪の残差とほぼ同じ大きさだ。
どのフレームで、どの行か: 2026-09-20
上の Duck Hunt の発見のうち二つに答えが出た。どちらの答えも、実機についてではなく 計器についてのものだ。
実機は模型の F を描いていた。相関が見ていたのは色の位相だった
ラッチ 900 の野原は動かない。nes-console の新しい frame-motion のプローブは、
ベンチの台本の SET と AT の行そのものを模型の上で演奏し、フレームごとに、
256 x 240 の活性なドットのうち何個が前のフレームと違うかを数える: 絵の 920 から
929 までは、ドット一つまで同一だ。あの記録の上でどんな測定をしても F-1 と F+1 は
見分けられなかった。二つは同じドットを描くからだ。
相関が代わりに見ていたのは色の副搬送波だった。同一のドットを持つ二つのフレーム は、復号した標本の 9 パーセントで違い、五つの候補は二つの組に分かれる: F-2、F、 F+2 が一つの値を読み、F-1 と F+1 が別の値を読む。小数第 4 位まで同じだ。それは フレームの偶奇であって、それ以外ではない。ぼかしでも取れない: 幅 1 ドットの箱型を かけても静止したフレームの 6 パーセントはまだ候補を分けており、副搬送波 1 周期ぶんの 幅のものでも 3.8 パーセント残る。縁に残るものは過渡であって正弦波ではないからだ。
そこで split-score に五つ目の測定が付いた。これは別々のことについての二つの
読みだ:
- 何が動いたか: マスクは PPU 自身のドットから来る (厳密で、構成上、位相には 盲目だ)。争っているドットは自分の 8 標本と両隣のドットを取り、争いの無い側は 5 ドット離れて立ち、絵は副搬送波 1 周期にわたってぼかしてから rms を取る。 位相はアライメントと一緒に回るので、どの候補も自分の一番良いアライメント、 前後 1.5 ドットの範囲で採点される。
- 色の位相: 復号した絵が与えるマスクの上で同じ計算をしたもの。これが名指しする のはフレームではなく偶奇だ。
どちらも合成の上で押さえてあり、MUTATE_FRAME=1 は両方で赤になる。この測定の
最初の版はそうではなかった: F に対して当てはめた位置合わせをそのまま使っていて、
スクロールするゲームではそれが、測ろうとしているまさにその違いを吸い込んでしまい、
変異体は緑で通った。直し方は、どの候補も争っていない所でだけアライメントを当てはめる
ことだ。
そして、その回のカモが上って行く二枚の捕捉、模型が 17 フレームにわたって毎フレーム
違う絵を描くところで (exercise/dh-duck.txt はラッチ 989、dh-duck-late.txt は
1020):
| 走行 | ラッチ | 何が動いたか | 次の候補 | 床 | 色の位相 |
|---|---|---|---|---|---|
20260919-235044 | 989 | F+0、rms 0.047 | 0.117 | 0.020 | F-1 0.100、F+1 0.107、F+0 0.183 |
20260919-235753 | 1020 | F+0、rms 0.047 | 0.158 | 0.020 | F+1 0.113、F-1 0.117、F+0 0.121 |
実機は模型の F を描いていた。絵二枚で、何が描かれたかによって。フレーム規則は、 行 249 でポーリングするゲームの上で成り立つ。色の位相は今も奇数側の隣を好み、 それは今では独立した項目だ。
その間の三つ目の走行 (20260919-235505、一つ目と同じ台本) は、どの候補にも
まったく合わなかった: 0.041 の床に対して 0.24。カモがよそへ飛んでいたからだ。床を
印字しているのは、まさにそのためだ。走行あたり捕捉は一つ、という点も同じ: スコープ
から 12 M ポイントを読むのに 4000 ラッチほどかかるので、同じ台本の中の二つ目の
TRIG はいつでも手遅れになる。
輝度の隔たりは色のものであって、行のものではない
capture-score に PROFILE=<colour> が付いた: 一つの色の輝度を行ごとに、模型と
実機の上で、模型がほかの何からも整定のぶんだけ離して描くドットについて出す。
一つの色の内側の傾きなら、それは絵のものだ。同じ行で色と色の間に段があるなら、
それは色のものだ。
| 記録 | 色 | 行 | 実機、平均で | 100 行あたりの傾き |
|---|---|---|---|---|
| Super Mario Bros. のタイトル | $22 | 1..207 | -0.0443 | +0.0001 |
| Duck Hunt の野原 | $21 | 1..148 | -0.0444 | +0.0006 |
| Duck Hunt の野原 | $29 | 157..225 | -0.0445 | +0.0318 |
| Duck Hunt の野原 | $18 | 183..239 | -0.0283 | -0.0023 |
一つの色が 207 行にわたって、絵の 3 分の 1 の高さから下端まで、漂わない: 100 行 あたり一万分の一だ。そして同じ行で、二つの色が -0.0445 と -0.0283 と読む。だから E2 の三つ目の発見に答えが出た: 行 200 以下の領域が外れが小さいのは、そこに住んで いる色のせいであって、どこに座っているかのせいではない。復元がフレームの下端で 弱くなるということは何も無い。
実機の上の E3 の仕掛け: 2026-09-18
E3 のうち手だけを除いた全部を、E2 の輝度に答えが出た晩に実機で走らせた: b3.py record の書く形で打ち込んだ記録 (exercise/e3-scripted.txt: メニューの Start は
200、タイトルの Start は 330、520 から Right、ジャンプ 4 回、1100 でパッドを離す)、
それをラッチ 400、800、1150 で捕捉しながら二度再生し (runs/e3-a、runs/e3-b)、
二つの再生を突き合わせ、0..1150 について二分探索する。実機に最初に出会ったのは
道具のほうだった: 構える処理がヘッドの拒む古い EXT トリガに落ちていた。状態の
問い合わせが、ヘッドが記録を読んで黙る 1 分のあいだに死に (しかも走行を演奏した
ままにしたので、以降の呼び出しはすべて拒まれた)、そして、ある捕捉を模型のものと
呼ぶ判断の規則が B1 の領域の許容だったので、実際の捕捉はどれも自分の較正のぶん
そこから外れた。規則はいま絵の相関で、split-score の新しい四つ目の節の二つの
読みだ: 画面は粗い形で (30 x 32 のブロック、0.99 以上)、フレームは全解像度で
(F が隣のどれより悪くない)。全解像度だけではどの画面かを言えなかった: ラッチ 400
でのゲームの黒いワールド画面は小さな文字で、模型のどのフレームに対しても 0.71 と
読むのに、実機が自分自身に対しては 0.999 と読む。二つの絵の連鎖は細かいところで
違い、ブロックはそれを平均して消す。
| ラッチ | 画面 | 実機と模型 | 二つの再生 |
|---|---|---|---|
| 400 | 黒いワールド画面 | 画面 0.9988、フレームは似ている (静止) | 一致 |
| 800 | 1-1、マリオが歩いている | 画面 0.9990、フレーム r 0.916 に対して隣は 0.689 | 一致 |
| 1150 | 1-1、パッドを離した | 画面 0.9992、フレーム r 0.921 | 一致 |
二つの再生の相関は小数第 3 位まで同一で返ってきた: このゲームはリセットからなら
実機の上で決定的だ。二分探索の最初の一歩、ラッチ 1150 が一致したので、0..1150 に
食い違いは無い。妨害は、AT 520 80 を落とした記録 (マリオは歩かない) に対して
ラッチ 800 の捕捉を採点するもの: 画面 0.66、食い違う。残っている段は手だ:
exercise/e3-hand.txt (MODE PASS、25 秒ほどのプレイ。UNO のブリッジの予定表が
128 個しか持てないので短い)、それから b3.py record、そしてそれが書いたものに
対する同じ再生、一致、二分探索。
E3、手一つ: 2026-09-19
この段のゲートは人のプレイの上で満たされた。記録は exercise/e3-hand.txt (電源を
入れ直し、MODE PASS、リセット一回、1 分間)。オーナーがブリッジを通した純正の
パッドで、マルチカートのメニュー、タイトル、そして 1-1 を旗まで遊んだ (走行
20260919-012524)。1 分のプレイはパッドのバイトが 289 回変わる。UNO のブリッジは
128 個までなので、b3.py record --until 1850 は最初の 31 秒 (127 回の変化) を
再生する。500、1000、1500、1840 で捕捉しながら二度再生し (runs/e3h2-a、
runs/e3h2-b)、一致を見て、400..1840 について二分探索した:
| ラッチ | 実機と模型、再生 a / b | 二つの再生 |
|---|---|---|
| 500 | 画面 0.9997 / 0.9993、タイトル (フレームは似ている) | 一致 |
| 1000 | 画面 0.9992、フレーム r 0.917 に対して隣は 0.899 / 0.900 | 一致 |
| 1500 | 画面 0.9993、フレーム r 0.927 / 0.863 に対して隣は 0.666 / 0.657 | 一致 |
| 1840 | 画面 0.9985、フレーム r 0.916 に対して 0.914 (マリオが立っている) | 一致 |
二分探索の最初の再生、1840 が一致した: 人が Super Mario Bros. を遊んだ最初の 30 秒のあいだ、実機と模型のあいだにラッチ一つぶんの食い違いも無い。
先に四つのものが壊れた。どれも手が見つけたもので、打ち込んだ記録が見つけたものは 一つも無い。この段はそのためにあった:
- ヘッドがブリッジを溢れさせた。
ATの行 125 本を続けて送ると UNO の シリアルのバッファが溢れた: あるものは# ?で返り、あるものは桁が欠けたまま 受け取られ (AT 2219 00がat 2210 00として)、コンソールはラッチ 1015 から 別の履歴を演奏したのに、どの検査も黙っていた。オーナーは画面でそれを見た (「逆向きに走っている」)。ヘッドはいま各行のエコーを待ち、食い違えば走行を止める (send_checked、d6ac715)。それから予定表だけの再生は、1965 ラッチ中 1965 で 記録と一致した。 - 記録がリセット前のセッションを抱えていた。
b3.py recordはブリッジの 記録全体を読んでいた。いまはブリッジの# resetから始める。 - 再生が間違った場所から始まった。 素の
RESETからの再生は、前の走行が 残したバンクのまま立ち上がってくる。ゲームの後ならそれはそのゲームだ: Super Mario Bros. はメニュー無しで再開し、メニューのための押しはゲームに行き、画面は 模型のものと 1 フレームずれて合い、二分探索はラッチ 1 まで歩いた (runs/e3h-*、 無効)。ラッチ 142 で実機のフレームを吐かせて見つけた (split-score DUMP=): 模型がメニューを見せている所に 1-1 があった。記録はいま、リセットより前の走行の 電源の語を保つ。 - 窓が短すぎて、予定表が小さすぎた。 25 秒はタイトルで終わった。1 分は UNO が
持てる量を超えるので、
--untilは前半だけを再生した。
同じ晩に、1 分ぜんぶ。 UNO の予定表を詰め込み直し (bench-v1b-uno.md の
「How the bridge works」: 項目を 2 バイトに、スケッチの文字列をフラッシュに、
128 個入っていたところに 600 個、スタックの余地は実測)、手の走行は旗まで再生され
た: 289 回の変化すべて、3601 ラッチ (exercise/e3-replay-full.txt、runs/e3f-a、
runs/e3f-b)。
| ラッチ | 実機と模型、再生 a / b | 二つの再生 |
|---|---|---|
| 1000 | 画面 0.9992、フレーム r 0.862 / 0.917 に対して隣は 0.848 / 0.900 | 一致 |
| 2000 | 画面 0.9989、フレーム r 0.917 に対して 0.735 | 一致 |
| 3000 | 画面 0.9985 / 0.9982、フレーム r 0.953 / 0.909 に対して 0.753 / 0.810 | 一致 |
| 3580 | 画面 0.9988 / 0.9992、旗 (フレームは似ている) | 一致 |
400..3580 についての二分探索は最初の再生で一致した: 人が 1-1 を旗まで遊ぶ 1 分の
あいだ、実機と模型のあいだに食い違いは無い。その道中でさらに三つ壊れ、どれも道具の
中だった。模型のフレームの上限 (2000 フレーム、およそ 1985 ポーリング) のせいで、
b3.py がそれをラッチに合わせて決めるまではラッチ 2000 より先を何も採点できな
かった。エコーされる 290 行を読み込むのに、走っているコンソールに対して 15 秒
かかったので、再生の TRIG 1000 はラッチ 1060 で発火し、60 フレーム進んだゲームの
捕捉二枚が食い違いとして読めた (ヘッドはいま、再生を読み込んでいるあいだコンソールを
リセットに保ち、ラッチが過ぎてしまったトリガを拒む)。そしてヘッドは一度、走行を
始める要求を取りこぼした。b3.py はいま、もう一度頼む前にそれを問い合わせる。
agree は二つの再生を、それぞれが着地したフレームで比べる。輝度はコンソールの
温まりのぶん違うからだ。
取り組み 1: 実機に合わせて調整する仮想スタック
模型はコンソールの絵ではない。スイッチのところで見たコンソールのチップたちであり、 高速ラングはそれに対して押さえられている。だからここでの「調整」は、見た目が 正しくなるまで振る舞いをいじることではない。模型が、自分では測れなかった数字を 抱えている数少ない場所を見つけ、その数字を実機で測り、どこから来たかを記録する ことだ。その場所がノブであり、それはすでにクレートのあちこちに散っている:
| ノブ | どこに住んでいるか | 今日 | 設定のためにベンチが何を測るか |
|---|---|---|---|
アライメント (cpu_phase、ppu_phase) | nes-console の Alignment | 一回の実測 (4, 3)、既定値 | 電源投入 100 回にわたる E4 のヒストグラム: その分布と、コンソールに好みがあるかどうか |
| 十進補正のオフ。最初の半サイクルでのスタックポインタ | v2a03-micro、6502 のラング 3 にある二つのノブ | ダイで実測、ピンのゴールデンに対して証明済み | 無し: これらはダイのもので、ベンチのものではない |
| RDY の立ち上がり | replay-recorded の RDY_RISE_SHIFT | 実験用のノブ、答えは出ている (コアは解放をピンより 1 サイクル後に見る) | 無し: ベンチにアドレスバスは無い |
| リセットの保持時間 | nes-glue、札の付いた仮の値 | 書き下ろし | E4: リレーが離れてから最初のフェッチまで、捕捉の上で |
テレビの各段 (CrtParams) | ntsc-crt | 書き下ろし、そう札も付いている | まだ無し: ベンチのテレビは測っていないし、グラバーは別のテレビだ |
| 符号化器のレベル依存の位相 | 未構築 | 彩度の高い色では、模型は色相で二つの目の両方から 12.6 度と 14.1 度離れている。場所は負荷の下での実機のアナログ出力 (eyes-vs-scope.md) | 目の負荷の下と、スコープだけの両方で捕捉したカラーバーのカートリッジ。定数は負荷ごとに当てはめ、MUTATE で赤 |
| DAC の負荷 | 未構築 | トリガ無しの捕捉は彩度が高く出た。プローブか DAC か未決 | カラーバーのカートリッジの終端した捕捉 (B1 の最初のゲート) |
| 実機の温まり | nes-console の [warmth] seconds_on と [warmth_curve] の depth、tau_s、2026-09-19 に構築 | 秒数はヘッドのログから実測。曲線は暖機の系列に当てはめた (depth 0.0214、tau 1050 s、rms 0.001) | 二つ目の暖機の系列と、カラーバーのカートリッジでの一本。あらゆるレベルが、利得なのかオフセットなのかを言う |
| 電源投入時の RAM | nes-console の [ram] fill または seed、2026-09-18 に構築 | 書き下ろし: 空白、あるいは一様な詰め物、あるいは種から作った模様。これを作るきっかけだった最初のゲーム (冷えた起動の後のマルチカートのメニュー) は、結局これを必要としなかった: ヘッドの WAIT を直したら、実機は冷えたままでも押しを受け取る (open-items.md) | 電源投入時の RAM、OAM、VRAM を見せる、私たち自身のカートリッジ |
これらを外へ出すのが、シートの破線の箱にある提案だ: 走行ごとに一つのファイル、
runs/<stamp>/ の中の knobs.toml を、模型のランナーが読む。どの鍵も、値がどこ
から来たか (measured、authored、fitted) と、それを設定した走行スタンプを
持ち歩くので、報告は自分の数字のどれが当てはめた数字に乗っているかを言える。
ランナーは知らない鍵を拒むので、打ち間違いが黙って既定値で走ることはできない。
当てはめたノブは、当てはめと一緒に自分の残差を印字する。そしてノブには MUTATE が
ある: 当てはめた値から動かせば、それを当てはめたゲートは赤にならなければならない。
でなければそのノブは何もしていなかったことになる。目で調整するものは何も無い。
機構の名前も無しに点数を良くしてしまうような当てはめを持つノブは書き下ろしのまま
置き、その食い違いは open-items.md へ行く。
2026-09-18 に構築、二つ目の一手。 ファイルは runs/<stamp>/ の中の
knobs.toml で、tools/knobs.py init が模型の実測の既定値と走行の ARM の行から
書き、nes-console のどのランナーも KNOBS=path から読む
(nes-console/src/knobs.rs、ベンチが書く平たい TOML の部分集合だけ、依存は無し)。
今日ある表は二つ、それぞれ source と by を持つ: [alignment] は、E4 が実機
から設定するまでは二つのダイのクロックの作り方から実測したもの。[capture] は、
台本から来るスコープの映像への窓で、採点器はここからチャンネルを取る。init は
上書きを拒む。読み手は、名前を知らない表や鍵 (ppu_phaze は自分を名乗る)、source
の欠落、残差の無い当てはめたノブ、範囲外の値を拒む。tools/knobs.py check は、
ある走行のファイルにその読み手を走らせる。模型が実際に効かせている唯一のノブは、
効くことが証明されている: 動かせば、最初のフレームの終わりでのスケジューラの CPU
半サイクル数が一緒に動き、そのテストの MUTATE=1 は赤だ。b1-score.py はこの
ファイルを持たない走行のためにそれを書き、その報告はいまノブとその出自から始まる。
E2 の二つの走行が印字しているのはそれだ。意図してこのファイルに無いもの: 色相。
E2 はそれを実機の上で二つのレベルで測ったが、点が二つでは残差無しに直線が引けて
しまうので、カラーバーのカートリッジがあらゆるレベルを与えるまで (E1)、数字付きの
未決の項目のままにしておく。リセットの保持時間は nes-glue の中の札の付いた定数の
ままだ: コンソールの中でまだ誰もそれを読んでいないし、どこにも届かないノブはノブで
はない。
温まり、2026-09-19 に構築: 最初の当てはめたノブ。 暖機の系列 (上の「輝度の
ばらつき」) は、模型が知り得なかったことを一つ残していた: 実機がどれだけ長く
電源の入った状態だったか、だ。実機の絵は温まるにつれて自分の同期に対して縮み、
どの走行も冷えた模型に対して採点すれば、それが最初の 30 分で育っていく輝度と彩度の
誤差として読める。それを二つの表が持つ。[warmth] の seconds_on は実測だ:
tools/knobs.py が走行のトリガを head.log から読み、あらゆる走行のログを
またいでその前の最後の power on を読み、ヘッドが一度も電源を入れていない走行
(前面スイッチを手で入れた場合) には何も書かない。そういう走行は冷えたものとして
採点される。[warmth_curve] は当てはめだ: 模型の絵の利得
1 - depth * (1 - exp(-t / tau)) を tools/warmth-fit.py が系列の捕捉 10 枚に
当てはめる。二つの領域の輝度と彩度を一つの曲線の下に、各系列自身の始まりは厳密に
解いて: depth 0.0214、tau 1050 秒、40 個の数字にわたって rms 0.00098、最悪
0.0023。capture-score は、カードの模型の前段に入れる前に、模型の符号化した
フレームを消去期間のまわりでその利得だけ縮める。同期とバーストには手を付けない。
そこから採点器のレベルと復号器の位相が出てくるからだ (tests/knobs.rs、
MUTATE_WARMTH=1 で赤)。
それが系列に何をするか、各走行のファイルにノブを入れてもう一度採点した結果
(warmth-fit.py --check): $17 で -0.039 から -0.048 へ、$22 で -0.042 から
-0.055 へ動いていた輝度の誤差が、いまは 45 分ぜんぶにわたって -0.041 と -0.042 に
留まる。ばらつきは 0.0035 と 0.0040 で、ノブ無しでの 0.009 と 0.013 に対してだ。
ノブ自身のゲート: 当てはめから離して depth をゼロにすれば、ばらつきが戻ってくる。
そして、当てはめが一度も見ていない記録が一つ: 2026-09-18 の朝の 200 mV の記録は、
前面スイッチでコンソールを 1 時間走らせたまま取ったもので (だからその秒数は書き
下ろしだが、50 分を過ぎれば曲線は平らなので、1 時間は厳密でなくてよい)、ノブ付き
で -0.043 と -0.046 と採点される。冷えた記録 152924 の -0.042 と -0.045 に対して
だ: あの晩が温まりのぶん引き離していた二つの記録が、千分の一まで一致する。残った
オフセット、輝度でおよそ -0.04、彩度で -0.05 から -0.07 は、模型に対する実機自身の
ものであり、E1 のカラーバーに属する。
当てはめが言わないこと。輝度だけで見ると彩度より深い曲線を欲しがり (0.024 に対して
0.018)、$17 の輝度だけで見ると $22 のそれより深い (0.030 に対して 0.023):
この漂いは純粋な利得ではなく、一部はオフセットのように振る舞い、二つの色では
その二つを分けられない。共有の曲線は四つすべてで採点器の許容の内側にあるので、
カラーバーのカートリッジのレベルが利得とオフセットを見分けられるようになるまで、
一つの利得として立っておく。色相は温まりと一緒に動かない (ずっと -3.1 度と -9.2
度) ので、このノブはそれに触らない。
取り組み 2: パッドから絵まで学び取るゲーム
コンソールの入力は一つ、各ラッチでのパッドのバイトだけで、出力は二つ、絵と音だ。 だからベンチにとってゲームとは、バイトの予定表からフレームの列への関数であり、 ゲームを学ぶとは、欲しいフレームに到達する予定表を見つけることだ。すでに機械に なっているもの: 模型は実時間の 19 倍で走り、パッドをラッチごとに受け取る。トレースは どのラッチもどの読み出しも書き出す。絵は、スコープの記録を復号するのと同じ信号経路を 通って出てくる。そして E2 は、どんな予定表でも選んだラッチで実機の上で確かめる道を くれる。新しいのは、その上に載る語彙だ。
- screen とは、名前の付いたフレームの類だ。 較正カートリッジでは、どの画面も
自分の帯の中で自分の名を名乗る。そのためにあのカートリッジは存在する。ゲームでは
特徴を、画素からではなく、そのラッチでの模型自身の状態から読む: 絵のチップが
見せていたネームテーブル、パレット、スプライトの組、そして基準のフレームとしての
信号経路を通した模型の絵だ。実機から来たフレーム (グラバーのもの、あるいは
トリガでの捕捉を復号したもの) は、
b1-score.pyがすでに採点しているやり方で screen に照合される: 領域、輝度、色相、彩度、それに基準のフレームに対する 正規化した相関を加えて。視覚モデルは札付け役として入ってきて、模型の絵から名前 (タイトル、メニュー、最初の面、ゲームオーバー) を提案する。名前は書き下ろしとして 扱い、照合は数値のままなので、間違った名前は間違った札であって、決して間違った 照合にはならない。 - 成功とは、ラッチの予算のうちに到達した screen のことだ。 ある始まりから
reach <screen> within <n> latches。始まりとは、電源投入か、走行の保存した窓だ。 episode は始まりから終わりまでの予定表であり、その結果は、到達した screen と、 到達したラッチだ。 - 模型で探し、実機で確かめる。 予定表は模型の上で探す。安く、画面も要らず、 何本も同時に走るからだ。そして見つかった予定表は、大事なラッチにトリガを置いて E2 の仕掛けを通して実機で演奏する。一致することがゲートだ。食い違いは、ゲームに ついての発見 (初期化されていない RAM やフレームのタイミングから種を取る。E3 が 名前で並べるもの) か、模型についての発見であって、どちらもその予定表より価値が ある。
- 進み方は、フレームが一番単純なところから始まる: 較正カートリッジのパッドの 画面、どのバイトもそれ自身が一つの画面で、対応は恒等写像になる。次にゲームの タイトルとその開始。次に、プレイヤーが最初に間違えられること。
足された単語: screen、episode、reach、そして視覚モデルがすることを指す
label。仕掛けは B1 と B3 に、そのまわりのループを付けたものだ。
取り組み 3: X 線と、コードのパターンの百科事典
トレースはすでに、X 線が必要とするものを運んでいる: CPU がフェッチしたすべての バイト、すべてのラッチ、パッドのすべての読み出し、絵のチップへのすべての書き込みと それが着地したドットと行、カートリッジ空間へのすべての書き込み、すべての割り込みの エッジ。ある動作の X 線とは、あるラッチのあるバイト一つだけが違う二つの走行 (ボタンを押した場合と、押さない場合) と、その二つのフェッチが最初に違う半サイクル だ。その半サイクルはあるルーチンの中の一命令であり、トレースの中でそれより下流の すべて (そのルーチンが書く RAM、届く絵のチップのレジスタ、切り替えるバンク) が、 その動作のコード経路になる。模型のランナーは、二つの走行と diff があれば今日これを できる。道具になるのは、その diff、ルーチンに名前を付けること、そして 6502 の ページがそれをダイの上で見せられるようにまわりを切り出した窓だ。
実機は自分のフェッチを見せられない。ベンチにアドレスバスが無いからだ (それは
bench-plan.md の 16 チャンネルのロジックアナライザの項目のままだ)。だから X 線は
模型のものであり、記録済みのバスによってダイに押さえられ、実機の上ではその動作の
結果で確かめられる: 次のトリガでの絵、ポーリングの数、音。それが正直な適用範囲で、
シートもそう描いている。
百科事典は、X 線が積み上がったものだ。ゲームができているパターンは数が少なく、 繰り返し現れる: ポーリングのルーチン (ストローブ、8 回の読み出し、ビットを RAM の 1 バイトへ、前のバイトに対する立ち上がり)、フレームのループと割り込みハンドラが 立てるフラグ、DMA のレジスタを通したスプライトの写し、スクロールの書き込みとそれが 着地するドット、バンクの切り替え、乱数、パッドのバイトから動作への振り分け、音楽 ドライバのティック。一つの項目は、パターンの名前、それが何をするか、その特徴 (触るアドレス、上げるイベントの種類、ダイで実測したサイクル数)、Halfshot の ページで立てるための窓、それが見つかったゲームを CRC で、そしてそれが教える機構だ。 最後のその欄が肝だ: 百科事典は、NES のゲームを作るための道具の土台になる。そこでは どのパターンにも私たち自身の走る例が付き、その例の X 線が、商用のゲームが見せたのと 同じ形を見せる。
一つの制約がどの項目の形も決めている。商用のカートリッジのバイト列は ROM の中身で あり、決して公開しない。だから項目はパターンの形 (アドレス、数、イベントの種類、 窓のオーバーレイの行) を運び、バイト列は決して運ばない。そしてコードを見せる実作例 は、ソースが私たちのものである ROM だ: 較正カートリッジ、一族のテストカートリッジ、 パッドのカートリッジ。トレースの道具も同じ理由で、商用のトレースは ROM の置き場所と 同じ所に置いている。
足された単語: xray、diverge (最初に離れた半サイクル)、pattern、そして
window。窓は T2 がすでに作ってある。
2026-09-18 に構築、三つ目の一手。 tools/xray.py rom.nes <name> --latch N --byte HH は、その ROM を nes-console の trace で二度トレースし (00 を保持
したものと、ラッチ N で HH を 1 ラッチだけ保持したもの)、二つの .pins の記録を
並べて流すのでどんな長さの記録でも収まり、そして報告する: diverge、最初に離れた
半サイクルと、そこで実行されている命令を 6502 のサイト自身の表 (web/disasm.js、
それが住む唯一の場所) から逆アセンブルしたもの。path、記録が違っているそれ以降の
すべての半サイクルを、命令と、それが何をしたか (書いた RAM、届いた絵のチップの
レジスタ、切り替えたバンク。ピンと、動作側の走行のイベントから) の札を付けた区間
として。path 本体は次のラッチで終わり、それより後の違いはそのバイトの反響として
報告される。rejoin、記録が最後まで再び一致する所、あるいは一致しないこと。特徴の
行一つ。そして --window を付ければ、6502 リポジトリの replay-recorded が食い
違いのまわりで切り出した動作側の走行の窓が、その中のオーバーレイの行も一緒に、
Halfshot のページが立てるために付く。不変条件は拒むことだ: パッドのバイトが CPU へ
入る戸は一つ、$4016 か $4017 の読み出しだけなので、それ以外の所で最初に違う
記録は同じ走行ではなかったということになる。そして、基準側の記録のフェッチした
バイト一つをラッチより前で壊す MUTATE=1 は、そこで拒まれなければならず、実際に
拒まれる。--bytes を付けなければ、報告はフェッチしたバイトと即値オペランドを
すべて覆い隠すので、商用のカートリッジの X 線はアドレス、ニーモニック、数、
イベントの種類を運び、そのコードは決して運ばない。トレースは ROM の置き場所と同じ
所へ行く。最初の二つの X 線と最初の二つの項目は encyclopedia.md にある: 自分の
コードごと載せたパッドのカートリッジのポーリングのルーチンと、Start でのマルチ
カートのメニューのバンク切り替え、こちらは形だけ。tools/dissect.py が同じ晩に
続いた: ゲームの 1 フレームを走査線の順で (ハンドラのベクタと RTI、行の付いた
すべての PPU 書き込み、DMA、それを書いているルーチン付きの VRAM の連射、$2002 の
空回り、パッドのラッチ、待ちのループ)、表をたどってジャンプエンジンを追ったルーチンの
呼び出し木、RAM の地図、コードのページ、そしてフレームごとに 1 行。そして
xray.py --script は二つの走行を台本の上に置く (ゲームへの入り方)。それらで最初に
分解されたゲームは Super Mario Bros. だった: mario-dissection.md と、百科事典の
項目 3 から 7。
今日どこまで機械か、そして最初の三手
| ピース | 2026-09-19 時点の状態 |
|---|---|
| 回帰テスト、両方の手 | 成立。scripts/hands-head.sh と scripts/hands-manual.sh |
| 台本一つ、両側 | 成立。実機でも模型のランナーでも SET と AT をラッチごとに |
| トリガと捕捉 | 成立。復号は ntsc-crt を通して |
| 模型のトレース、記録済みのバス、ダイのページの上の窓 | 成立 (T0 から T3) |
| 比較器 | compare-logs.py と b1-score.py は実機に出会った (E2、二度)。b3.py も (E3 の仕掛け、2026-09-18、打ち込んだ記録で)。終端した捕捉はまだ E1 のもの |
| E3 | 2026-09-19 に人のプレイで満たした。旗までの 1 分ぜんぶ: 二つの再生が一致し、実機はどの捕捉でも模型と一致し、食い違いは無し (上の節) |
| E2 | 2026-09-18 に二度演奏した: 最初に落ちた領域を名指し ($17、どの軸でも)、色相の外れは 0.1 度まで再現、輝度の外れはスコープの 1 レベル分の幅 (上の節) |
| 較正カートリッジ | 模型の側は済み、実機の側はフラッシュチップを待っている |
| ノブのファイル | 2026-09-18 に構築: tools/knobs.py、nes-console/src/knobs.rs、出自付きの表二つ、名指しの拒み、スケジューラに届くことを証明したアライメントのノブ (取り組み 1) |
| screen と探索 | 提案。episode は E2 以来の単語 |
| X 線の diff と百科事典 | 2026-09-18 に構築: tools/xray.py (diverge、path、rejoin、窓。拒むことは MUTATE で証明)、項目七つの encyclopedia.md。tools/dissect.py と mario-dissection.md、最初に分解したゲーム (取り組み 3) |
最初の三手を順に: まずゲームでの E2。存在しないものを何も必要とせず、どの取り組みも その内側で走るループだからだ (上で演奏済み)。次にノブのファイル。E4 の最初の実測の ノブが、ソースの編集ではない着地点を必要とするからだ (取り組み 1 で構築済み: 捕捉の 窓は入っていて、色相は意図して入っていない)。そして、ソースが私たちのものである パッドのカートリッジでの X 線の diff。最初の百科事典の項目 (ポーリングのルーチン) を コードごと公開できるからだ (取り組み 3 で構築済み。二つ目の項目も同じ晩にマルチ カートの Start を X 線した)。三手すべて打ち、E3 も満たした (2026-09-19、上の節)。次に来るのは課程自身の 次の段、マスタクロックにプローブを当ててからの E4 の掃引、そしてフラッシュチップが 届いたときの E1 だ。
ビルド時に nes-bench/docs/exercise.md から取り込んだもの。リポジトリが唯一の写しだ。報告は独自の作業用の言葉をいくつか使う: 報告書で使われる言葉。