6502tinymachines

実機のコンソールを捕捉する

M4 の報告。作業の後に書かれた。

走行スタンプ: 2026-09-01、このリポジトリの五番目のコミット、rustc 1.97.1、 cargo test --workspace は 56 テストが緑、clippy は清潔、MUTATE=1 は 33 を 赤にする。M5 がこれらの計数を check-self-counts の走査の下に置く。

閉じたもの

ntsc-source-cap: 入りは、宣言はされているが信用しないレートの生の波形、出は グリッドに乗った放送の CompositeFrame。位相を導出するのではなく稼がなければ ならない、唯一のソースだ。復元は四つの測定でできている:

  1. 同期。 実測した同期の先端 (sync tip) に固定した閾値 (第 1 パーセンタイル より上に、表 1 の 286 mV の深さの半分) での立ち下がりの縁を、公称のライン 周期で歩き、それから一致したすべての縁にわたって最小二乗で周期を出す。その 周期を宣言されたレートと比べたものが、そのままレートの食い違いの測定だ: 13.5 MHz の捕捉に注入した +50 ppm は、5 ppm 以内で復元される。
  2. フィールド。 ほとんど低いままのラインが広パルス (broad pulse) であり、 最初の一群がフレームのライン番号を固定する。(RGB の符号化器は、このために フレームのライン 4..7 と 266..269 にライン粒度の広パルスを生やした。 モジュールの文書に記録がある: それが無いと、M2 の符号化器には検出器が 見つけられる垂直方向の構造が無かった。)
  3. バーストのロック、ラインごと。 バーストの窓をグリッドの上に再標本化し、 幾何自身の目標 (原点 Phase(0) で burst = -sin) に対して射影し、標本より 細かいずれを実測した位相の残差で補正し (代数は残差をずれの符号を反転した 位置に置くので、補正は引き算になる)、0.01 グリッド標本を下回るまで繰り返す。 バーストの無いラインと広パルスのラインは、直前にロックしたずれを借りる。
  4. 再標本化とレベル基準合わせ。 ラインの全体を、ロックしたずれのところで 8 タップの窓付き sinc 補間に通し、それから実測したバックポーチを消去期間に 留めることで DC を取り除く。

capture_model は仕様の言うキャプチャカードの代役で、原則 5 の下にある試験 装置のコードだ: 折り返し防止ローパス (6.5 MHz、実在のカードは持っている)、 宣言したレート誤差、DC のオフセット、そして種を与えた決定的な雑音。どの パラメータも、各テストの呼び出し側で述べる。

往復のゲート

  • 4 x f_sc での清潔な捕捉: レート誤差の実測は 3 ppm 未満、バーストの残差は 0.05 グリッド標本未満、グリッド上の比較の平均は 4 mV 未満 (0.5 IRE ほど)。 最悪の 60 mV はバーの辺に限られていて、そこでは模型の折り返し防止フィルタが、 元の信号がまだ運んでいる実際の信号を取り除いている: これは計器の床であり、 そう述べてある。
  • 13.5 MHz での汚れた捕捉 (+50 ppm、+20 mV の DC、2 mV の雑音): 50 ppm は 5 以内で見つかり、DC はレベル基準合わせで消え、グリッドの平均は 6 mV 未満、 復元したフレームは入力の映像レベルの 0.04 以内でバーに復号する (M2 の 0.02 より緩いのは、この信号がキャプチャカードを渡ってきたからだ)。
  • フィールドの構造は、符号化器が置いた場所で見つかった: 復元したライン 4 と 266 はほとんど同期の低のまま、ライン 25 はそうではない。

捨てなければならなかったミューテーション

最初の MUTATE の摂動は、捕捉の全体を副搬送波の 4 分の 1 周期ずらすものだった。 同期はほとんど動かないのに、クロマは 90 度間違うはずだという理由付けだ。 バーストのロックはそのずれを、設計どおりに吸収し、テストはずれ自身の補間の 損失のぶんだけ、わずかに赤になった。対象が生き延びるように作られている ミューテーションは、何も証明しない。差し替えた版は、対象の中のバーストの ロックを無効にする (recover_with。その文書は、あのテストは偽で通り得る ということだけを言う)。すると復元は同期の縁だけに頼ることになり、二つの往復は どちらも許容の 5 倍から 7 倍で赤になる (平均 29 mV と 38 mV に対して、許容は 4 と 6)。これが、ロックが飾りではなく荷重を受けていることの証明だ。

ゲートの、開いている半分

仕様の M4 を閉じるゲートには二つの部分がある。合成の往復は上で閉じた。 実機の録音は開いていて、ハードウェアが要る: この機械には捕捉された NTSC の 波形が一つも無く、どこか別の場所でそれを合成することは、このリポジトリ自身の コードをファイル一つに通して洗い、それをオラクルと呼ぶことになる。6502 リポジトリのタッチの教訓がそのまま当てはまる: 捕捉が動いていると信じる前に、 デバイスに問い合わせよ。ハードウェアができたときに必要なもの: カラーバーを 映していて、既知の公称レートで生の標本 (WAV か f32) として録ったコンポジット NTSC のソースが何かあればよく、既にある recover と M2 のバーのテストが ゲートを閉じる。

持ち越し

  • M5: セルフカウント、ライセンスの登録簿、まとめたダイバージェンスの一覧、 そして仕様の v0.3 草案。
  • 上の、実機の録音のゲート。ハードウェア待ち。

追記、2026-09-02: 取り込みの経路、組んで待っている

ディレクターがキャプチャカードを持っているので、実機のゲートの機械の側はこれで 揃った (crates/ntsc-source-cap/src/ingest.rs、examples/recover-real.rs、 tests/real.rs):

  • WAV (PCM 8/16、float 32。レートはヘッダから) と生の f32/i16/u8 (レートは 宣言) の読み取り。
  • 任意の ADC の単位からボルトへの自動レベル合わせ。定規は同期の深さ (表 1: 先端は消去期間より 40 IRE 下)、二つのレベルはヒストグラムの最も低い二つの峰 として見つける。実機のファイルが触れる前に証明した: 利得 0.31、オフセット 0.7 に掛け直した合成の捕捉は、レベルが戻り、グリッド上で平均 7 mV 以内で 復元する (arbitrary_units_auto_level_and_recover)。
  • ゲートのテストは captures/real-bars.* が存在するまで、名指しで飛ばす。 REQUIRE_REAL=1 はそれを必須にする。実機の録音について述べた許容: バーの 各チャンネルが映像レベルの 0.08 以内 (民生の機器は、表 1 のスタジオの +/-1 IRE より緩いところに座る)。清潔な捕捉がそこから外れたなら、それは 広げる数字ではなく、調べるべき発見だ。

人の側は 1 ページだ: docs/capture-instructions.md。ファイル一つとコマンド 一つで、最後のゲートが閉じる。

追記、2026-09-02 の晩: 実機のコンソールの最初の捕捉と、復元に欠けていた NES のプロファイル

実機のフロントローダー式 NES (Super Mario Bros. / Duck Hunt のカート) からの 捕捉を五本、一族の DS1054Z の上で tools/scope-capture.py を使って取った: それぞれ 125 MSa/s で 12 M 点、1 記録あたりおよそ 5.8 フレーム、リポジトリの 外に控えを取った。発見が三つ、どれも同じ日に直して証明した:

  • スコープは、止まっている間のメモリ深さの設定を無視する。 捕捉の道具は それを止まった状態で出していた。設定は AUTO のままで、読み返しには記録長が 無かった。今は走っている間に設定し、それを表明する。
  • 復元のヒストグラムは、量子化されたデータの上で櫛になった: 整数の u8 の 標本が整数より細かい区画に入ると、同期の先端が二つの峰として読め、「消去 期間」は先端より 1 カウント上に見つかった。区画の幅は今、データ自身の 量子化の刻みに留めてある。合成の f32 の証明用の捕捉では、このどちらも 捕まえられなかった。
  • 復元はどの捕捉も放送の NTSC として復号していた。そして NES は放送の NTSC ではない。 そのラインは副搬送波 227 と 3 分の 1 周期 (2730 ではなく 2728 グリッド標本) なので、放送が 1 ラインあたり半周期進むところを、バーストの 位相は 3 分の 1 周期進む。それが、vsync の行 245..=247 に固定された 262 本の 順次ラインにわたって続く。放送として復号すると、どのラインも直前より 2 グリッド標本ぶん余計に回って着地した: フレーム全体を下っていく滑らかな色相の 転がりで、ほぼ白黒のメニュー画面では見えず、World 1-1 では見逃せない。 放送だという前提は、スコープのクロックも -740 ppm と測り間違えていた (2730 のうちの 2 は 733 ppm だ)。NES のプロファイルの下では、同じ記録が -7 から -19 ppm と測れる。直したのが recover_nes だ: NES の幾何、符号化器自身の wave-8 の方形波から導いたバーストの目標、そして転記した表の絶対ボルトでの レベル。だから復元したフレームは、オラクル自身の定数で復号する。合成の NES の 捕捉で証明した (原点 8、0、4 を鎖にして、固定されるフレームが原点 0 のものに なるようにした): クロマは点ごとに 0.006 以内、帯の輝度の平均は 0.005 以内。 同じ捕捉に対する放送の復元が組み込みのミューテーションであり、これは 10 倍 外れるか、拒まなければならない。

一族自身の合成に対して採点した最初の領域 (examples/score-real-region.rs。一時停止した World 1-1 の空と、同一の デコーダを通したべた塗りの $22 のフレーム): 輝度は 0.003 以内、色相は 0.6 度 以内、そして彩度は 28 パーセント高い (0.573 に対して 0.448)。最後の数字は、 広げるべき許容ではなく発見だ: 終端していないプローブでの走行がクロマの振れを 良く見せているのか、それとも実際の DAC の AC の振れが本当に表の DC で測った レベルを超えているのか。75 オームで終端した録り直し一本が、どちらかを決める。

バーのゲートは開いたままだ: あの二本入りのカートは SMPTE のバーを描かないので、 captures/real-bars.* は今もテスト ROM を待っている。控えに入れた五本の捕捉 (メニュー、一時停止した World 1-1、タイトルと序盤のデモ、Duck Hunt のメニュー、 Duck Hunt のプレイ中) は端から端まで、見て分かる形に復号する。取っておく一本は 一時停止した 1-1 のフレームだ: 平坦で安定した $22 の空が、実物のシリコンから 出て、このリポジトリの経路を全部通ってきている。

三つ目の追記、2026-09-04: 終端器

設計した実験を走らせた: 同じコンソールを、スコープの入力に付けた 75 オームの フィードスルー終端器へ。CH3 は DC、200 mV/div、125 MSa/s で 12 Mpt (captures/loaded-a、画面はカートリッジのメニュー)。図とその数値は、その記録と、 以前の終端していない一時停止した World 1-1 の記録に対して tools/composite-figures.py を走らせたもので、docs/composite-figures.json に 書き出してある (コミットされる測定。記録そのものは gitignore されたベンチの データのままだ)。

終端したとき、記録のどのラインでも: 同期の先端 -0.076 V、消去期間 0.124 V、 バーストは尖頭間 0.280 V。つまりバーストは、同期から消去までの段の 1.40 倍に 振れる。転記した表自身の比は 1.42 だ。終端しないと同じコンソールは、同期から 消去まで 0.50 V、バースト 1.00 V、比は 2.0 と読めた: DC のレベルは 2.5 倍に、 3.58 MHz の副搬送波は 3.6 倍に上がっている。これが、デコーダの濾過より前で クロマをおよそ 40 パーセント良く見せているプローブでの走行であり、上の 28 パーセント高い彩度の出どころだ。このコンソールの 75 オームへの絶対レベルは 表のものより下に座っている (同期から消去まで 0.264 V に対して 0.200 V、その 同期の先端はゼロボルトより下)。復元が表の同期と消去期間へ正規化しているのは、 まさにそれを吸収するために作られている。ライン周期は 63.512 us と測れ、放送の 63.556 に対して、NES の 2730 のうち 2728 のグリッド (63.509 us) にあたる。

終端した、一時停止した World 1-1 の空を同一の経路で採点することが、この追記が まだ運んでいない確認だ: コンソールでもう一度 Mario を一時停止させる必要がある。

四つ目の追記、2026-09-06: レベルを、信号から読む

コンソールの N6 の捕捉のゲート (nes の capture-score: full_palette.nes の 平坦な領域すべてを、カード模型と auto_level_nes に通し、コンソール自身の 合成に対して採点する) が、レベル基準合わせが区画一つぶん粗いことを見つけた: 1 V の幅に 256 のヒストグラム区画なら一つ 4 mV で、両端でそれぞれ同期の深さの 1.4 パーセントにあたり、それがフレーム全体にわたる 3.7 パーセントの輝度の 利得として読めた。さらに、絵の標本の半分が消去期間より下に座るべた塗りの 暗い色 (表の 1 行目の低いレベル) が、ヒストグラムの二番目の帯を埋め、まるごと 消去期間と取られた。find_tip_blank は今、ヒストグラムを同期の閾値を置くため だけに使う。同期の先端はすべての同期パルスの内側の中央値、消去期間は各パルスの 手前のフロントポーチの中央値で、そこは絵が占めることができない。模型の上では: 消去期間は 0.01 mV 以内、同期の深さは 0.003 パーセント以内 (nes_auto_level_finds_the_levels_finer_than_a_histogram_bin。古い検出器は これを 75 mV 外す)。上で採点した空の領域は、作りからして変わらない: u8 の記録の 中央値はその量子化された値であり、峰が与えたのと同じものだ。0.2.4 としてタグを 打った。

五つ目の追記、2026-09-06: フロントエンドを、両側に

コンソールの N6 のゲートは、バーのフレームの平坦な領域すべてを、カード模型と recover_nes に通し、同一のデコーダを通したコンソール自身の合成に対して 採点した。そして 436 領域のうち 69 で色相と彩度の許容を外し、その原因の クロマ残差を、この模型の折り返し防止ローパスに位置づけた: 符号化器のクロマは 方形波であり、フィルタはその高調波を取り除く。生の合成を食わされたデコーダは、 1 周期 12 標本で第 11 と第 13 が基本波に折り返して乗ったものを見る。だから 二つの側は、同一のデコーダに違う基本波を手渡す。欠陥はどちらの側でもなく、 比較の手順にあった: 捕捉と比べる合成は、その捕捉の帯域制限を帯びていなければ ならない。front_end は模型のローパスだけを取り出したものだ (tests/front_end.rs: グリッドのレートでの模型とフレームごとに一致、DC の利得は 1、色の第 3 高調波は消えて基本波は残る)。そして、どの捕捉の比較でも リファレンスの側はまずそこを通る。0.2.7 としてタグを打った。

六つ目の追記、2026-09-06: コンソールのバーのカートリッジが見つけたもの

両側にフロントエンドを入れ、デコーダのクロマのフィルタから導いた領域の余白で 採点すると、blargg の full_palette のバーはまだ狭すぎた (両側に 5 ドットの 整定距離があるのに 16 ドット)。そこでコンソールのリポジトリが、32 ドットの セルを持つバーのカートリッジを書き下ろした (nes の testrom::bars_program): 一つの輝度の行に置いた 12 の色相と背景色で、行は 2 秒ごとに進む。輝度の行 1、2、3 では、どの領域も計画の許容を守った (最悪で輝度 0.0001、色相 0.3 度、 彩度 0.0012)。輝度の行 0 では、どの色相もライン上のバーの位置とともに増える 回転で外れ、復元は 0 ppm の捕捉でレートを 145 ppm 間違えて報告した: 最も暗い 色の低いレベルは、カードのフィルタを通った後、副搬送波の半周期にわたって同期の 閾値の下へ潜り、その谷のどれもが、ライン歩きには同期の縁に見えていた。同期の 縁は今、横切った後に 1 マイクロ秒の低いレベルが続くことで資格づける (tests/front_end.rs。輝度 0 のバーを 5 ppm で、2 ppm 以内で復元。以前は 312 も 違っていた)。実機の記録は変わらない。0.2.8 としてタグを打った。

七つ目の追記、2026-09-18: 垂直同期の形はダイのものだ

NES の符号化器の垂直同期は、ライン粒度で書き下ろされていた (行 245 から 247 を ドット 0 から低に、ドット 254..286 に空白の窓)。復元の広パルスの検出器には それで足り、一度も実測されていなかった。ベンチが、どのラインもその同期から位置を 決めた二つの画面で、実物のコントローラのポーリングを模型のものより 0.6 ライン 早いと見つけたので、スイッチレベルの 2C02 に訊いた (2c02 の vsync-probe: DAC の同期先端の脚を、1 フレームを通して半ステップごとに読む): 広パルスが三つ、 それぞれ行の水平同期が始まる場所から始まり、行 244、245、246 にあって、次の行の ドット 253 まで続く (表のドット番号で。DAC は水平でも垂直でも、どの同期も カウンタの番号より 3 ドット後に見せる)。23 ドットのセレーション (刻み) の消去が あり、244 から 246 にバーストは無く、行 247 は普通の同期とバーストで閉じる。 実機のコンソールの記録も同じものを示す (広パルスは、直前の水平同期から ちょうど 1 ライン後に、0.934 ライン分の長さで、1 ライン間隔で三つ)。 ntsc-source-nes::segment は今その形を出し、the_vertical_sync_is_the_dies が縁を一つずつ押さえている。復元のバーストの無い行は 245..247 から 244..246 へ 動いたが、その固定に使う縁 (行 244 のもの) は変わらないので、復元されたどの行も 番号を保ち、実機の記録は前と同じに採点される。古い立ち上がりは 64 ドット (0.19 ライン) 遅かった。0.2.10 としてタグを打った。

八つ目の追記、2026-09-18: レベルを、コードの間で読む

auto_level_nes は同期の先端と消去期間を中央値として読んでいたが、量子化された 記録の上では中央値は丸ごとのコードになる。ベンチの記録は 1 目盛り 200 mV の u8 で、そこではコンソールの同期の深さは 22.5 コードなので、中央値は 22 か 23 しか 言えず、採点するどの輝度も帯びる利得が、ポーチの真のレベルが半コードを越える たびに 4.5% 動いた。ベンチの暖機の連続がそれを見つけた: 一つの静止したタイトル 画面を半時間にわたって 10 回捕捉すると、領域の輝度が、5 分離れた二つの捕捉の 間で 0.012 の段差を作り、そのままそこに留まった。一方で記録そのものは 100 分の 1 コードしか動いていない (消去期間の平均は 88.48 から 88.51、中央値は 88 から 89)。レベルは今、各帯の中央 80% の平均だ。中央値と同じように辺やリンギングに 強く、しかも刻みより細かい。 nes_auto_level_reads_between_the_codes_of_a_quantised_record が押さえている (同期の深さを 22.5 コードに量子化した合成の記録で、消去期間はあるコードから 0.45 のところに置く: 10 分の 1 コード以内で読み、深さは 0.5 パーセント以内。 中央値を使う MUTATE_LEVEL=1 は 2.2% で赤)。連続なボルトでのテストは、作り からしてこれが見えなかった。0.2.11 としてタグを打った。

九つ目の追記、2026-09-19: フレームの位相が何であれ、絵は合う

NES は各フレームを、3 分の 1 周期ずつ離れた三つの副搬送波の位相のどれかで始め、 それはフレームごとに巡る。recover_nes はどのラインのバーストも原点 0 に ロックし、phase_at_origin として 0 を返していた。だから 4 や 8 で始まった フレームは、その前提を本当にするために、どのラインも 4 または 8 グリッド標本 (半ドットか 1 ドット) ずらされていた。色は正しく復号した。ロックがバーストを デコーダの期待する場所に置くからだ。そして平坦な領域はどれも前と同じに採点 された。合成のテストの鎖は、固定されるフレームが原点 0 のものになるように 組んであった。それを見つけたのは絵全体の相関だ: nes の分割採点は、自分の合成の 往復と模型自身のフレームの間で 0.77 と読み、4 標本ずらすと 1.0000 になった。 そして実物の最初のスクロールする記録は、ずらさずに 0.93、2 標本ずらして 0.95 と 読んだ。復元は今、まず原点を測り (各ラインを、その同期の縁が置く場所に置いた ときのバーストの誤差を、4 の倍数に丸めて、フレーム全体での投票で決める)、 そこにロックし、フレームの中でそれを名指しする。 the_recovered_picture_sits_on_the_encoder_s_whatever_the_frame_s_origin が 押さえている (各原点を順に、輝度は符号化器のものから 1 標本以内に戻り、原点は 名指しされる。MUTATE_ORIGIN=1 は 4 標本ずれている)。0.2.12 としてタグを 打った。

ビルド時に ntsc-crt/docs/m4-report.md から取り込まれる。写しはリポジトリの一つだけだ。報告は独自の作業用の言葉をいくつか使う: 報告書で使われる言葉。