6502tinymachines

信号経路の仕様

仕様、バージョン 0.3。

信号レベルの NTSC 符号化、復号、そして CRT 表示のシミュレータ。Rust の コア、純粋に CPU、WASM にコンパイルされ、キャンバスへ RGBA を書く。どの段 も外部のオラクルに照らして検証されている。NES のアーケードシェル (256x240 のネイティブキャンバス) と halfphi のエンジンのラダーの相棒だ。

2026-09-02 にディレクターが批准した。実装するエージェントが起草した変更 一覧 (docs/spec-v0_3-draft.md) から。v0.2 (docs/ntsc-crt-handoff-v0_2.md) は記録として残してある。

この文書の数字の状態: v0.2 のものと違って、いまはどれも実測か確認済み であり、それぞれ走行スタンプを持つ節目の報告への指し先を携えている。数え られるものは tools/check-self-counts.py の下にある。v0.2 が主張を先に 計算していた箇所は、テストがそれを確認したか、この改訂が訂正した。三つは 生き残らなかった (以下の 3.2、4.2、5 節。訂正と印を付けてある)。

v0.2 からの変更

  • 3.2 節の NES のフィールドレートを訂正: 60.0988 Hz は二フレームの平均で あって、フルフレームのレート (60.09848 Hz) ではない。NES の三つのレート はいずれも厳密に書いた。[m0-report]
  • 4.2 節の帯域制限の段を、いま手元にある一次規格に照らして訂正: Y は無制限、 色差は等帯域。I/Q の分割は、ST 170M 自身の中でそれが占めている歴史的な 注記の位置に降格した。[m2-report]
  • 5 節のラング D を訂正: 二フレームの時間方向のコムは打ち消せない (剰余は 120/240 度で、180 度にはならない)。正直なラングは、フルフレームの模様に 対する三フレームのものだ。[m2-report]
  • 4.2 節に、M4 のフィールド検出が必要とするブロードパルスの垂直同期の行が 加わる。4.3 節は、復元が実際にどう測るかを記録する。
  • 7 節の書き下ろしの見積もりを、実測の表と、名指しした最適化のてこに 置き換えた。
  • 8 節の節目は、それを閉じた記録を携える。9 節はいま未決の問いを載せる。 v0.2 の未決の問いはすべて解決した。

1. 設計の原則

  1. コンポジットの標本が原生の単位だ。 黙って再標本化するものは無い。 どの段も入力の格子と出力の格子を明示的に宣言し、食い違いは実行時の警告 ではなく、型システムを通したコンパイルエラーになる。
  2. 三つのソース、一つの波形の型。 NES のドットの流れ、一般の RGB フレームバッファ、実物の捕捉した波形は、すべて一つの CompositeFrame に集まる。下流はすべてソースを問わない。
  3. どの段にもオラクルと、失敗する道がある。 MUTATE=1 はフィルタの 係数かレベルの表を乱す。ゴールデンとの比較は赤にならなければならない。 失敗できない検証は検証ではない。対象が生き延びるように設計されている 妨害は何も証明しない (捕捉のソースの最初の妨害は、まさにこれで捨てた。 m4-report)。
  4. 実測と書き下ろしは分けたままにする。 信号のレベルの表、行列の係数、 フィルタのタップは、出典を挙げた元から、出自のヘッダ付きのデータ ファイルへ取り込む。コードに手で打ち込むことはしない。
  5. テスト装置もコードだ。 比較へ至る途中で再標本化、変換、しきい値 処理をするものは何であれ、試験されるパイプラインと同じ出自と、許容の 同じ根拠を携える。(M1 の欠陥のうち三つと M2 の三つは装置の側にあり、 どれも直したところに記録されている。)
  6. 文章に em ダッシュは使わない。元データは元のライセンス条項を携え、 ファイルごとに NOTICE.md に記録する。

2. クレートの配置

ntsc-crt/
  crates/
    ntsc-grid/        標本格子の型、位相の算術、フレームのジオメトリ (規約)
    ntsc-source-nes/  NES PPU のドットの流れ -> CompositeFrame
    ntsc-source-rgb/  RGB フレームバッファ -> CompositeFrame (ST 170M の完全な符号化器)
    ntsc-source-cap/  捕捉した波形 -> CompositeFrame (同期ロック、バーストロック、再標本化)
    ntsc-decode/      CompositeFrame -> YuvFrame -> LinearRgbFrame (ラング A/B/C/D)
    ntsc-crt/         LinearRgbFrame -> DisplayFrame (ビーム、走査線、残光、マスク、ジオメトリ)
    ntsc-oracle/      ネイティブ専用、テスト専用: blargg のバインディング、ゴールデンの生成、
                      比較用のリサンプラ、記録済みの整列
    ntsc-testgen/     合成の NES ドットの流れと RGB のテストフレーム (PPU は不要)
    ntsc-wasm/        wasm-bindgen のブリッジ、RGBA 出力、ドリフトを数えるフレームの歩調
  goldens/            走行スタンプ付きの記録済みゴールデン (生成物、gitignore)
  data/               レベルの表、行列、タイミング、フィルタのタップ。それぞれ出自付き

ntsc-grid は、エンジンのラダーで v6502-pins が果たす役割を果たす: 他の すべてのクレートがそれに照らして検証される、共有の規約だ。ntsc-testgen のおかげで、M1 から M3 は、スタックのどこにも PPU が存在しないまま閉じられ た。意図どおりだ。

v0.2 からの名前の変更が一つ、復号のクレートに記録されている: 中間表現は YiqFrame ではなく YuvFrame だ。実装されているのが等帯域 YUV であり、 ST 170M-2004 の 7.2 項がそれを主たる符号化としているからだ (4.2 節)。


3. 格子の規約 (ntsc-grid)

3.1 標本レート

基本の格子: 色の副搬送波の一周期あたり 12 標本。

  • 副搬送波 f_sc = 315/88 MHz = 3.579545... MHz。厳密な比 (39,375,000/11 Hz) として保持し、浮動小数にはしない。ST 170M-2004 の 11.1 項 (f_sc = 5 MHz x 63/88) と nesdev wiki のリビジョン 23864 に 照らして確認した。
  • 標本レート = 12 x f_sc = 472,500,000/11 Hz = 42.954545... MHz。
  • NES のマスタクロック = 6 x f_sc。一ドットは副搬送波の一周期の 2/3 に わたり、それは 8 標本だ。

三つのソースはどれもこの格子の上でフレームを作る。捕捉のソースはこの格子へ 再標本化する (4.3 節)。他の二つはこの上で最初から生成する。

3.2 フレームのジオメトリ (訂正)

ジオメトリのプロファイルは二つ、ソースによって選ばれる。以下の剰余はすべて ntsc-grid/tests/residues.rs が確認し、check-self-counts.py が再計算す る。ただ引用されただけのものは一つも無い。

NES のプロファイル

  • 一行あたり 341 ドット x 8 = 2728 標本。有効域は 256 ドット x 8 = 2048。
  • 一フレーム 262 行、プログレッシブ。描画を入れた奇数フレームは pre-render の行からドット 340 を落とすので、その行は 2720 標本になる。格子の型は 行ごとの標本数を持つ。
  • 行の剰余: 2728 mod 12 = 一行あたり 4 標本 (120 度)。三行のクロマの 模様だ。
  • フレームの剰余、フルフレーム: 714,736 標本、mod 12 = 4。描画を切って いれば位相は三フレームごとに繰り返す。
  • フレームの剰余、短いフレーム: 714,728 標本、mod 12 = 8。フル、それ から短いで進むのは 4 + 8 = 0: 描画を入れていれば位相の模様は二フレーム ごとに繰り返す。
  • レート、厳密に (v0.2 は最初の二つを混同していた。m0-report):
    • フルフレーム: 29,531,250/491,381 Hz = 60.09848 Hz;
    • 短いフレーム: 8,437,500/140,393 Hz = 60.09915 Hz;
    • 描画を入れたときの二フレームの平均: 39,375,000/655,171 Hz = 60.09881 Hz。文献がふつう「60.0988」として引く数字だ。三つは field_rates_exact が別々にピン留めするので、もう混同できない。

放送のプロファイル (RGB と捕捉のソース)

  • 一行あたり副搬送波 227.5 周期 = 2730 標本。525 行、インタレース、262.5 行の二フィールド。
  • 行の剰余: 2730 mod 12 = 6 (180 度)。フレームの剰余: 1,433,250 mod 12 = 6。合わせて、標準の四フィールドの列になる。
  • フレームレートは 30,000/1,001 Hz。フィールドレートは 60,000/1,001 = 59.94006 Hz で、ST 170M の 11.3 項と桁まで一致する。正典の数字が 315/88 とジオメトリから落ちてくること自体が一つの検査だ。
  • レベル、同期、消去、バーストのタイミングは ST 170M-2004 から (一次資料 そのもの。pub.smpte.org から取得し、ハッシュでピン留めした。 data/broadcast-timing.toml)。

3.3 位相は導出され、与えられない

どの標本での副搬送波の位相も、ジオメトリの純関数だ: phase_at_origin、 プロファイル、フレームのパリティ、行の番号、標本の番号。ntsc-grid は Geometry::phase_at と line_len を出していて、どのソースもそれを使う。 位相を入力として受け取るソースは無い。別の開始位相が必要なソースは、唯一の 自由パラメータである phase_at_origin を変える。この規約への整列を自分 で稼がなければならないのは、捕捉のソースだ (4.3 節)。

3.4 中心となる型

v0.2 が定め、そのとおりに作られたもの: SampleRate (厳密な比)、Phase (0..12)、FrameParity (Even、OddFull、OddShort。OddShort は放送の プロファイルでは名指しで拒まれる)、行ごとの CompositeLine { samples, sync_start, burst_start, active_start } を持つ CompositeFrame、CompositeSource、そして Stage<In, Out>。レベルは コンポジット出力でのボルトだ。IRE は導出される表示上の単位で、境界で変換し、 保持はしない。


4. ソース

4.1 NES (ntsc-source-nes)

入力: 341 x 262 のドットごとの (colour_index: u6, emphasis: u3) と FrameParity。位相は Geometry::phase_at から。ドットごとの符号化: レベル の表から 8 標本。色相は 12 位相の矩形波で、正弦波ではない。

データ: data/nes-levels.toml。nesdev wiki のリビジョン 23864 にある終端 付きの実測 (lidnariq) で、二重書き写しのゲートを通して受け入れた (エージェント二つ、同じリビジョン、43 個の数値、食い違い 0。 tools/diff-transcriptions.py として再実行できる)。

オラクル: blargg の nes_ntsc 0.2.2、composite のプリセット。zip の sha256 は tools/fetch-oracle.sh でピン留めしてある (死んだ正式 URL の 2011-08-12 の Wayback の捕捉だ)。ntsc-oracle の中でネイティブにビルドし、 出荷は一切しない。彼の色の生成も一行ずつ移植してあり (blargg_model.rs、 LGPL、テスト専用)、512 項目すべてで彼自身のコンパイル済みパレットに 1 カウント以内で押さえている。だから比較の定数はオラクルのソースの文面から 来る。

比較の装置は原則 5 に従う: リサンプラの傾きは blargg 自身の 3 入力/7 出力 の塊のジオメトリ (出力画素あたり 24/7 標本)。そのオフセットとバーストの 対応は実測して凍結し、掃引を記録してある (m1-report)。そして一つの剛体回転 (ばらつき 0.028 度) が、復調した (U,V) を彼の (I,Q) へ写す。これはテストが 走行ごとに当てはめ直す。食い違いはすべて m1-report で名指しして帰属させ、 docs/divergences.md に集約した。「十分近い」で閉じたものは何も無い。

4.2 RGB フレームバッファ (ntsc-source-rgb) (訂正)

入力: 0..1 の映像レベルの G'B'R'、または sRGB のバイトを sRGB の EOTF と 基準カメラの OETF に通したもの (ST 170M の 5.1 項)。連鎖は以下、括弧内は 項番号:

  1. 映像信号に対する基本行列 (6.1。data/yuv-matrix.toml。一次資料に 照らして桁まで確認した)。
  2. 帯域制限 (v0.2 の「Y 4.2、I 1.3、Q 0.4 MHz」から訂正): Y は無制限 (7.1)。色差の信号は等帯域 (7.2: 1.3 MHz で 2 dB 未満の落ち、3.6 MHz で少なくとも 20 dB の落ち。7.3 に従いほぼガウシアン)。I/Q を分けた帯域幅 は規格自身の NTSC-1953 の継続注記であり、既定ではない。実装したフィルタ は Blackman 窓の sinc で、7.2 の雛形に対して最も余裕が出るよう掃引して 選んだ。実測した減衰量は data/filters/rgb-encoder.toml に記録してある。
  3. 2271 標本の有効域へ再標本化する (実際には 2 と順序を入れ替えている。 帯域制限済みの内容では同一で、そう記録してある)。
  4. Geometry::phase_at の位相で変調する。セットアップ付きの 10 項の基本式 (0.925 Y + 7.5 + 0.4552 (B-Y) sin + 0.8115 (R-Y) cos) で、黒 7.5 / 白 100 IRE を基準とするデコーダに対して自己正規化する。
  5. 同期、消去、バーストは表 1 と表 2 に従う。バーストは基準副搬送波から 反転 (8.2)。フィールドごとのバースト無しの九行も守る。フレームの行 4..7 と 266..269 にブロードパルスの垂直同期 (v0.3 での新規): 行の粒度 で、半行ごとに同期幅の刻み一つ。M4 のフィールド検出器には十分だ。規格の 半行の等化の構造は、記録された非目標だ。
  6. 二つのフィールドへインタレースする (行の対応は書き下ろし、記録済み)。

オラクル: 答えの分かっている信号。75% バーの波形の平均は、公開された波形 モニタの列と、10 項からの導出の両方に着地する (76.9 / 69.0 / 56.1 / 48.2 / 36.1 / 28.2 / 15.4 IRE。二つは互いに照らして比べた)。バーストは 40 IRE p-p、クロマの振幅は 10 項の尺度から 3% 以内、復号の往復は 0.02 以内。許容は使う場所で根拠を示した (m2-report)。

4.3 捕捉 (ntsc-source-cap)

入力: 宣言されたレートの生の標本の流れ。信用するのは百万分率までだ。典型的 なレートは 4 x f_sc か 13.5 MHz で、どちらも試験した。復元は四つの測定だ (m4-report):

  1. 同期: 実測した先端に固定したしきい値での立ち下がりエッジを、公称の周期 で歩いてから最小二乗。当てはめた周期を宣言されたレートと比べたものが、 そのままレートの不一致の測定になる (注入した 50 ppm を 5 以内で見つけた)。
  2. フィールド: ブロードパルスの群がフレームの行番号を固定する。
  3. 行ごとのバーストロック: バーストの窓を格子へ再標本化し、その位相を ジオメトリ自身の目標 (原点 Phase(0) でバースト = -sin) に照らして測り、 標本より細かいオフセットを格子の 0.01 標本より下まで反復する。ロックを 無効にする妨害によって、荷を負っていることが証明されている。
  4. 行全体を再標本化し (8 タップの窓付き sinc)、DC を実測したバックポーチに 合わせ直す。

オラクル: RGB のソースからキャプチャカードの模型を通して作った合成の捕捉 (折り返し防止のローパス、レートの不一致、DC オフセット、種付きの雑音。装置 のコードで、呼び出しごとにパラメータを述べる)。綺麗な往復は格子の上で平均 4 mV 以内、汚した往復は 6 mV 以内で、バーへの復号は 0.04 以内。ゲートの 実物の録音の半分は開いていて、ハードウェアを待っている (9 節)。


5. 復号 (ntsc-decode) (ラング D を訂正)

ラング。どれも構築時に選べて、どれも別々に検証されている:

  • ラング A: ノッチ。 クロマは生成した帯域通過で取り (f_sc での符号付き の応答を記録し、正規化に使う)、ルマはその厳密な補だ。両方のプロファイル で使える。
  • ラング B: 2 ラインのコム。 放送のプロファイル専用で、180 度の行の剰余 に乗る。バーでのルマのクロマ帯のエネルギーは、生の行の 2% を下回るまで 落ちる。NES のプロファイルでは構築時に名指しで拒まれる: 120 度の剰余 のため、二行の差はクロマの 0.866 をずれた位相で通し、半分をルマに残す (v0.2 がエージェントに計算させた係数で、tests/combs.rs で実測した)。
  • ラング C: 3 ラインのコム。 放送は (1,2,1)/4、NES は (1,1,1)/3 で、 これが本来のコムだ: 0/120/240 度の三行を足すとゼロになる。縞のフレームで 示した: コムのルマに残る副搬送波に同期したクロマは、生の行のものの 1% 未満で、同期復調で測った (帯のエネルギーはクロマではない: ドットの レートのルマの矩形波がクロマの帯の中に住んでいて、コムはそれを正しく 残す。m2-report)。
  • ラング D (訂正): 時間方向のコム、NES のプロファイル。 v0.2 は、二 フレームの平均が 180 度のフレームの関係を通してクロマを打ち消すと主張して いた。3.2 節の剰余 (120/240 度) がそれを禁じ、測定も同意する: 二フレーム はクロマの経路を 0.866 に減衰させ、基本波の半分をルマに残す。批准された ラングは、フルフレームの模様に対する三フレームのもの (描画を切った 状態で、剰余はどれも 4。Battletoads の場合だ) で、垂直方向の代償なしに 厳密に打ち消す。代わりに動きが滑る。二フレームの測定はテストとして残して ある: v0.2 がこのラングに求めていた、OddShort を復号に通す運動になるから だ。

分離のあとは、すべてのラングが一つの尾を共有する: Geometry::phase_at から 来る位相にソースのバーストの軸のオフセットを足した位置で、sin/cos による QAM 復調、U/V のローパス、書き写した逆行列、表示のガンマ (2.2、記録済み)、 そして同じ水平の格子の上の LinearRgbFrame へ。

フィルタは FIR で、タップは data/filters/*.toml にある。 tools/gen-filters.py が生成し、カットオフ、窓、タップ数、そして実測した 応答を、符号付きで記録する。手で調整したタップは受け付けない。

オラクル: NES のソースには blargg (4.1 節)。RGB のソースにはバーの既知の 答えと往復。捕捉のソースには合成の捕捉の往復。MUTATE=1 はクロマのタップ 一つと行列の係数一つを乱し、復号のゴールデンは失敗する。リサンプラと バーストは、常に入っている赤の証明を携えている。


6. CRT (ntsc-crt)

決まった順序の五つの段。どれも任意だ: ビーム (水平のガウシアン。ここで格子 が画素になり、その比はフレームに書かれる)、走査線 (垂直のガウシアンで、幅は 画素の輝度とともに広がる。ブルームと電流の関係は模型化したパラメータで、 書き下ろしとして文書にしてある)、蛍光体の残光 (チャンネルごとの指数減衰で、 壁時計ではなくソースのフレーム周期で進むので、再生はビット単位で決定的だ)、 マスク (整数のピッチのアパーチャグリルの三つ組、タイル張りは整数のみ)、 ジオメトリ (樽型と角の丸め。既定では切ってある。224x224 の可視が保証される 窓はライブラリの不変条件で、window_visible)。

オラクル: 外部のものは無い。このクレートは模型であり、そう述べている。各段 は自分が宣言した数式に照らして押さえられ、その宣言はテストの中で型になって いる。MUTATE=1 はビームの幅と時定数一つを乱す。仕様が名指しした二つの 乱しだ。実測した費用: 尺度 3 で五つすべてを入れて 1 フレーム 52.7 ms (m3-report)。


7. WASM ブリッジ (ntsc-wasm) (見積もりを実測に訂正)

入口は一つ: ドットの面を入れ、RGBA を出す。simd128 あり、なしの両方で ビルドし、どちらも実測した。

レートとドリフト。 三つのレートが関わっていて、どれも一致しない: NES の 60.09881 Hz (描画を入れたときの平均、3.2 節)、放送の 59.94 Hz、そして ディスプレイのリフレッシュだ。ブリッジは時間方向には再標本化しない: ソース は自分の厳密なレートで走り、ブリッジはコールバックごとに最後に完成した フレームを差し出す。重複したフレームと落ちたフレームは数えて外に見せる。 期待値は打ち直したものではなく導出したもので試験した: 60 Hz のディスプレイ はちょうどうなりの周波数ぶん落とす。ちょうど 30,000/1,001 Hz のディスプレイ は一度もドリフトしない。

見積もりを実測で置き換える (Ryzen 5 5600X、一スレッド、m2-report): NES のノッチはネイティブのフルパイプラインで毎秒 7.54 フレーム、wasm で 4.87。comb3 は 8.49 / 5.20。simd128 のフラグは雑音の内側だ。v0.2 の 「ラング A は SIMD 付きなら一スレッドに余裕で収まるはずだ」は生き残らな かった: 素朴な FIR は標本あたり約 253 回の積和を要する。てこは名指ししてあり、 正しさの節目の間は意図して作らなかった: U/V のローパスの前での間引き (復調の後のクロマは 0.6 MHz 幅だ)、それから行の畳み込みでの明示的な SIMD だ。


8. 節目、閉じたとおりに

ゲート記録
M0: 格子と規約閉じた、コミット ca09c95。剰余のテストと二重書き写しのゲート (43/0)。
M1: NES の符号化、ラング A、blargg閉じた、b8c057d。食い違いはすべて帰属させた。移植はコンパイル済みパレットに 1 カウント以内で押さえた。
M2: RGB の符号化、ラング B/C/D、WASM、ベンチマーク閉じた、0ad9320。一次規格が手に入り、ラング D は測定で訂正、スループットは実測。
M3: CRT の段閉じた、ca69c8e。解析的なテスト。記録したゴールデンは端から端までビット単位で決定的に再生する。
M4: 捕捉合成の往復は閉じた、8f7ae94。実物の録音は開いていて、ハードウェアを待っている。
M5: 文書とセルフカウント閉じた、5af32f0。check-self-counts.py の下に 56 の主張。NOTICE.md は出典ごとのライセンスの台帳。ダイバージェンスは集約した。

9. ディレクターへの未決の問い

  1. M4 の実物の録音のためのハードウェア: バーを映しているコンポジット NTSC のソースなら何でもよく、既知の公称レートで生の標本として捕捉した もの。recover と既存のバーのテストで、最後のゲートが閉じる。
  2. NES のアーケードシェル: それはどこに住むのか。そして ntsc-wasm の 規約 (ドットの面を入れ、RGBA と歩調の統計を出す) は、その必要に合って いるか。
  3. 写真の往復 (v0.2 の 5 節の PSNR と D65 下の CIEDE2000 の下限。決めた が、どのゲートも使っていない): 将来の節目に畳み込むか、落とすか。

v0.2 以降に解決したもの

  • v0.2 の 9 節の三つの問いはすべて: blargg はハッシュで 0.2.2 に固定、 D65 下の CIEDE2000 を選んだ (まだ使われていない。上の問い 3)、ラング D は M2 で取り、取る途中で訂正した。
  • v0.2 への訂正それ自体: 3.2、4.2、5 節で、m0-report と m2-report の測定 からここで批准した。

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