目 対 スコープ
2026-09-12 に追加。コンソールのコンポジット映像はいま、同時に二方向へ行く: 一方はスコープで、その記録が一族自身の復号器の材料になる。もう一方はベンチの Raspberry Pi につないだ USB のフレームグラバー (取り込み器) で、同じ信号についてテレビのチップ自身が持つ意見だ。tools/eyes.py が両方をまとめて取り、二つの絵をコンソールの画素グリッドの上で採点する。このページはその仕掛け、絵、そして最初の比較が言ったことである。
仕掛け
- コンソールは NES-001 で、中に入っていたゲーム (Super Mario Bros. と Duck Hunt が一つのカートリッジ) を走らせ、コンポジット出力は RCA ジャックから出る。
- そのジャックに T 分岐。 片脚はスコープの CH3 へ (DC、200 mV/div、1x プローブ: ntsc-crt 自身の捕捉が採点されたのと同じチャンネル)、もう片脚はグラバーの黄色い入力へ。75 オーム負荷は終端されている側の端に付くので、スコープの入力は、あの捕捉が採点されたときのままだ。
- グラバー: Pi に挿した Roxio Video Capture USB のスティック (
/dev/v4l/by-idの下の安定した名前を使う。カメラが小さい番号を取るまでは/dev/video2だった)。専用にビルドしたドライバを通す (後述)。コンポジット入力、720 x 480 のフレーム出力、カラー。 - スコープ: ベンチの LAN にいる Rigol DS1054Z、50 MSa/s で 1200 万点、240 ms、1 記録あたりおよそ 14 フレーム。ツールは触る前に設定をまるごと保存し、後で戻すので、同じスコープを使うもう一つの実験が乱されることは決してない。
- Pi がグラバーとカメラを持ち、ssh でワークステーションに応える。ワークステーションが復号器、ツール、記録を持つ。

グラバー、そしてそれが動くようになるまで
このスティックは EM2980 と刻印された Empia のチップで、チップ id 146 を報告する。どの Linux カーネルもそれを知らなかった。既存のドライバは unknown em28xx chip ID (146) と記録し、スティックの音声側だけを登録する。2014 年のカーネルのメーリングリストのスレッドが、そのファームウェアからチップに Conexant 級の復号器が内蔵されていることを推測したところまでで止まり、Arch のフォーラムの投稿は、誰も動かせないだろうと結論していた。
その後で変わったのは、2026 年に mainline の Linux が、まさにそういう内蔵復号器を持つ EM2828X 系列を、ブリッジ自身のレジスタ経由で駆動する形で、新しい Hauppauge USB Live2 のために取り込んだことだ。EM2980 はその系列の一つ隣の番号である。だからベンチのドライバは mainline の em28xx を Pi の 6.12 カーネルに対して out of tree でビルドしたもので、パッチが当たっている: 7.x のソースが 6.12 でビルドできるようにするシム二つ、チップ id、内蔵復号器と USB id を持つボードの項目、そして既定でのバルク転送。スティックのアイソクロナスのエンドポイントは、どの設定でも各フレームの上から 6 分の 1 しか届けないからだ。

バルクなら絵が届く。それが最初に見せたのは、コンソールの横のテレビが同時に映していたカートリッジのタイトル画面だった:


一晩が、限界ではなかった限界に消えた。ドライバは 480 行のうち 400 行しか届けていないように見え、復号器の設定が書くレジスタを一つずつ試しても何も変わらなかった。原因は「どの規格を検出したか」という問い合わせに対するドライバの答えだった: 内蔵復号器は規格を検出できず、既存のコードは「全部」と答え、捕捉ツールはその答えをそのまま使用する規格として返していた。625 行のビットが入ったマスクは、ドライバに 576 行のジオメトリと 480 対 576 の垂直スケーラを設定させ、240 行のフィールドがそれぞれ 200 行になって届いた。ffmpeg は問い合わせをしないので、いつでもフレーム全体を受け取っていた。差が現れたのはそこだ: 二つのプログラムの ioctl を並べて追跡した。ドライバはいま、いま有効な規格を答え、どちらもフレーム全体を流す。パッチ、ピン留めしたビルドスクリプト、そして全経過はリポジトリの head/roxio-em28xx/ の下にある。
試験
python3 tools/eyes.py pair --scope <ip> --pi <host> <name> # 両方の捕捉を、まとめて
python3 tools/eyes.py compare <name> # 復号、位置合わせ、採点
pair は ntsc-crt の捕捉ツールと同じようにスコープを設定し、Pi に ffmpeg 経由でグラバーのフレームを八つ求め、それが入った瞬間にスコープを止める。下の走行では停止が最後のフレームから 1 ミリ秒の内に着地したので、記録の最後の 240 ms とフレーム群は同じ絵だ。compare は記録を recover-real --nes (NES のプロファイル、書き写した表自身のレベル) で復号し、グラバーの最初のフィールドを取り (コンソールは 240p なので、どのフィールドも絵の全体だ)、両方をコンソールの 256 x 240 のグリッドに載せ、グラバーの水平方向の窓と垂直方向のオフセットを、仮定ではなく輝度の相関で見つける。報告するのは平均絶対差、平坦なブロックの一致 (端でのクロマのフィルタが数に入れられない場所)、彩度の高い画素での色相の差、そして最も食い違う平坦なブロック十個をコンソールの座標で。三面の絵と JSON を捕捉の隣に書く。captures/ は git が無視する。ここにある絵はその写しだ。
同時に取った最初の組が言ったこと (実測 2026-09-12)
Super Mario Bros. のワールド 1-1、コンソールは待機中。



復号器はレート誤差 -3.5 ppm で記録を復元し、バーストの残差の最悪は 0.17 グリッド標本だった。グラバーのコンソール画素 0 はその 720 のうち標本 44 に座り、復号器のフレームより 4 行低い。それを見つけた相関は水平に 0.95、垂直に 0.996 なので、位置合わせは疑いようがない。
| 値 | |
|---|---|
| 240 のうちの平坦なブロック | 平坦 159 個。平均絶対差 255 のうち 7.6 |
| 色相、グラバーから復号器を引く、彩度の高い 55,733 画素で | 中央値 +2.0 度 |
| 彩度、グラバー対復号器 | 1.05 |
| 絵の全体、R, G, B の平均絶対差 | 18.9, 14.2, 7.7 |
最悪の平坦なブロックはどれも空だ: 復号器はそれを (137, 125, 255) と読み、グラバーは (121, 114, 255) と読む。青はどちらでもクリップしていて、グラバーは赤と緑で 15 ほど低く、それが絵の全体で一様だ。つまり Roxio のチップの黒レベルかコントラストの設定であって、色相の誤りではない。色相は 2 度、彩度は 5 パーセントで一致し、それが絵の作業が気にしていた数値だ。復号器の色相が仲間外れなのではない。
絵の全体の差は端のものだ。グラバーのクロマは 4:2:2 で、復号器のものではないフィルタを通るので、すべての垂直な端が差の面でハローを持つ。だから平坦なブロックの数値こそ読むべきもので、だから動いているゲームは被写体として間違っている: 二つの捕捉の間の 1 秒の動きは、復号の食い違いとまったく同じに見える。
これが何のためかと言えば、グラバーは同じ信号についての第二の意見だ。二つが一致する場所では、どちらもおそらく正しい。平坦な色で二つが違う場所では、JSON がその色とブロックを名指しし、問いはどちらの復号器へでもなく、プローブを持ってコンソールへ行く。
三方向、模型にカートリッジを入れて (実測 2026-09-12、title1)
カートリッジは同じ日にリーダーから上がってきた (card-plan.md。ダンプのチェックサムはデータベース自身のもの、D26EFD78)。そしてコンソールがその基板、マッパー 66 を得たので、模型が同じバイト列を走らせられるようになった。eyes.py compare title1 --model <the dump> --frames 180 は模型のタイトル画面を、スコープの記録と同じ復号の連鎖を通して描き、両方の目に照らして採点する。


まず二つの目、タイトルの上で: 平坦なブロックは 255 のうち 0.57 で一致、色相は彩度の高い 16,606 画素で中央値 0.7 度、彩度は 6 パーセント以内、輝度の相関 0.99。これがグラバーの校正であり、動いているゲームが与えたものより厳しい。
次に模型を、それぞれに照らして:
| 平坦なブロック、平均絶対差 | 色相の中央値、彩度の高い画素 | 輝度の相関 | |
|---|---|---|---|
| 模型 対 復号器 | 255 のうち 0.67 | 12.6 度 | 0.95 |
| 模型 対 グラバー | 255 のうち 1.26 | 14.1 度 | 0.95 |
平坦なブロックは一致する。タイトル画面の大半は黒だからだ。色相は一致しない。ロゴの茶色は模型で (148, 92, 0)、スコープ側で (132, 73, 0)、グラバー側で (121, 69, 0)。文字のシアンは模型で (59, 200, 251) に対して (45, 200, 205) と (67, 202, 202)。一つの信号に対する二つの別々の復号器である二つの目が 1 度で一致するところで、模型は同じ色について 12 度から 14 度離れて座る: シアンではより青く、茶色ではより暖かい。
再走行 2026-09-13、CPU とスプライト DMA を直した模型で (その話はカートリッジのページにある): 同じコマンド、同じ捕捉。模型 対 復号器は平坦なブロック 255 のうち 0.66、色相の中央値 12.61 度、輝度の相関 0.95。模型 対 グラバーは 255 のうち 1.25、14.1 度、0.95。小数第二位まで同じ数字なので、色相は CPU のものでも DMA のものでもない。変わったのはフレームそのものだ: 最初の走行で下線の上に見えていた小さなスプライト、どちらの目もその瞬間に持っていなかったものが消え (DMA が写した古いスプライトのページだった)、動く人物はコンソールのものと同じ場所に立っている。上の三面の絵は再走行のものだ。
つまり仲間外れは模型の側の絵の連鎖であって、コンソールでもどちらの目でもない。絵の作業 (N6) は合成の往復で色相の外れを記録し、それをカードの模型のフィルタに帰していた。実物そのものが比較の向かい側に立ったのはこれが初めてで、それは模型の色相について二人の証人を立てて同じことを言う。まだ言えていないのは連鎖のどこかということだ: 符号化器のパレットの位相か、模型が合成するバーストの位相か、復号器が自分の合成を読む読み方か。それが次の問いで、いまそれに照らして答えるべき測定がある。
色相はどこに住んでいるか (実測 2026-09-13)
三方向の比較が開いたまま残した問いは、模型の絵の連鎖のどの段が 12 度から 14 度を負っているのかだった。答えは、同じ色を二つの段で測り、それからダイに訊き、それから同じ画面の二つ目の記録に訊くことから来た。
復号器ではなく、信号の段。 hue-stage (ntsc-crt のリポジトリ、crates/ntsc-source-cap/examples/hue-stage.rs) は、実際の捕捉の平坦な領域と、同じ色を符号化器に通したものを取り、両方を二度測る: 復号器を一切挟まずコンポジットの標本から直接射影したバーストに対するクロマの位相と、その一つの復号器を通した色相だ。title1 では、ロゴの茶色 ($17) は信号で 1.8 度、復号器を通して 3.1 度違う。文字のシアン ($2c) は信号で 14.4 度、復号器を通して 15.2 度。復号器は何も足していない: それが何であれ、すでに信号の中にある。
ダイでもなく、表でもない。 スイッチレベルの 2C02 の DAC ゲートは、書き写したレベルの表を標本ごとに押さえていたが、標準ワールドのパレットが持つ色相は 1, 2, 4, 6, 7, 8, 10 だ: ダイは色相 12 の画素を一度も見せられていなかった。輝度 2 で色相 1 から 12 までをそれぞれ一色ずつ持つワールド (2c02 リポジトリの crates/v2c02-dots/tests/dac.rs の every_hue_speaks_the_transcribed_table) は、あらゆる色相を DAC の前に置く: どれも 7,680 標本で不一致ゼロ、色相 12 のダイ上の波は 111111000000、表のものは 111111000000。模型のデジタルの連鎖はダイのものである。
色相 12 でもない。 同じメニュー画面を 2026-09-02 に記録したもの (smbdh-a、125 MSa/s、出力にはスコープだけ) は、シアンを表から 4.2 度、茶色を 4.2 度に置く。一つの定数だ。SMB のタイトルの記録は空 ($22) を 1.1、ロゴを 0.9 に置く。Duck Hunt の空 ($21) は 3.5、草 ($29) は 2.4。実物で測ったどの色相も表から約 4 度の内側に座る。例外は title1 の記録のシアンで、そこでは茶色は 2 に座っている。
負荷の下での実物の出力。 実際の波を副搬送波の位相で畳むと、模型の方形波がそうでないものが見える: 実物では立ち上がりに位相 6 つほど、立ち下がりに 3 つほどかかり、低レベルは表のものより 1 ボルトの 10 分の 1 だけ下に座り、シアンの 1.05 V のプラトーは丸まっているのに茶色の 0.80 V のものは丸まっていない。これは 2C02 の既知の差動位相歪みだ (nesdev の "NTSC video")。PPU の出力インピーダンスはレベルに依り、基板の容量が高い側の端をなまらせ、実効の色相が電圧とともに回る。明るい色のほうが大きく回る。wiki はそれを、時定数が電圧に従う RC ローパスとして模型化している。それを模型の波に当てると、amount 4 で行 2 を 14.7 度回す (シアンの 14.4 に当たる)。しかし行 1 は 7.5 度回すのに対し、茶色の実測は 1.8 だった。そしてそれは、測定がそうであるように、一つの行のすべての色相を同じだけ回す。title1 のセッションが以前の記録と違って持っていたのは、プローブと同じ出力に付いたグラバーだ: 実物の出力段にかかる別の負荷、一つの定数では合わない、より強く、より非線形なスルーである。
だから段は、目のセッションの負荷の下にある実物のアナログ出力、DAC の下流であり、模型はそれをまったく持っていない。復号器、符号化器の表、ダイは疑いが晴れた。三方向の中央値 12.6 度は、その負荷の下での、彩度の高い画素のうちシアンの文字が占める分だ。これを閉じるのは、一族のカラーバーのカートリッジを目の負荷の下で捕捉し、もう一度スコープだけで捕捉すること、12 色相を 4 行ずつすべて取ること、そして wiki の段を符号化器に入れ、その一つの定数を負荷ごとに合わせ、赤になる妨害を付けることだ。
仕組みの出どころ: nesdev wiki の NTSC video のページ (差動位相歪みの節とそのフィルタ)。
ビルド時に nes-bench/docs/eyes-vs-scope.md から取り込まれる。写しはリポジトリの一つだけだ。報告は独自の作業用の言葉をいくつか使う: 報告書で使われる言葉。