信号経路の仕様
仕様、バージョン 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. 設計の原則
- コンポジットの標本が原生の単位だ。 黙って再標本化するものは無い。 どの段も入力の格子と出力の格子を明示的に宣言し、食い違いは実行時の警告 ではなく、型システムを通したコンパイルエラーになる。
- 三つのソース、一つの波形の型。 NES のドットの流れ、一般の RGB
フレームバッファ、実物の捕捉した波形は、すべて一つの
CompositeFrameに集まる。下流はすべてソースを問わない。 - どの段にもオラクルと、失敗する道がある。
MUTATE=1はフィルタの 係数かレベルの表を乱す。ゴールデンとの比較は赤にならなければならない。 失敗できない検証は検証ではない。対象が生き延びるように設計されている 妨害は何も証明しない (捕捉のソースの最初の妨害は、まさにこれで捨てた。 m4-report)。 - 実測と書き下ろしは分けたままにする。 信号のレベルの表、行列の係数、 フィルタのタップは、出典を挙げた元から、出自のヘッダ付きのデータ ファイルへ取り込む。コードに手で打ち込むことはしない。
- テスト装置もコードだ。 比較へ至る途中で再標本化、変換、しきい値 処理をするものは何であれ、試験されるパイプラインと同じ出自と、許容の 同じ根拠を携える。(M1 の欠陥のうち三つと M2 の三つは装置の側にあり、 どれも直したところに記録されている。)
- 文章に 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 項)。連鎖は以下、括弧内は 項番号:
- 映像信号に対する基本行列 (6.1。
data/yuv-matrix.toml。一次資料に 照らして桁まで確認した)。 - 帯域制限 (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に記録してある。 - 2271 標本の有効域へ再標本化する (実際には 2 と順序を入れ替えている。 帯域制限済みの内容では同一で、そう記録してある)。
Geometry::phase_atの位相で変調する。セットアップ付きの 10 項の基本式 (0.925 Y + 7.5 + 0.4552 (B-Y) sin + 0.8115 (R-Y) cos) で、黒 7.5 / 白 100 IRE を基準とするデコーダに対して自己正規化する。- 同期、消去、バーストは表 1 と表 2 に従う。バーストは基準副搬送波から 反転 (8.2)。フィールドごとのバースト無しの九行も守る。フレームの行 4..7 と 266..269 にブロードパルスの垂直同期 (v0.3 での新規): 行の粒度 で、半行ごとに同期幅の刻み一つ。M4 のフィールド検出器には十分だ。規格の 半行の等化の構造は、記録された非目標だ。
- 二つのフィールドへインタレースする (行の対応は書き下ろし、記録済み)。
オラクル: 答えの分かっている信号。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):
- 同期: 実測した先端に固定したしきい値での立ち下がりエッジを、公称の周期 で歩いてから最小二乗。当てはめた周期を宣言されたレートと比べたものが、 そのままレートの不一致の測定になる (注入した 50 ppm を 5 以内で見つけた)。
- フィールド: ブロードパルスの群がフレームの行番号を固定する。
- 行ごとのバーストロック: バーストの窓を格子へ再標本化し、その位相を ジオメトリ自身の目標 (原点 Phase(0) でバースト = -sin) に照らして測り、 標本より細かいオフセットを格子の 0.01 標本より下まで反復する。ロックを 無効にする妨害によって、荷を負っていることが証明されている。
- 行全体を再標本化し (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. ディレクターへの未決の問い
- M4 の実物の録音のためのハードウェア: バーを映しているコンポジット
NTSC のソースなら何でもよく、既知の公称レートで生の標本として捕捉した
もの。
recoverと既存のバーのテストで、最後のゲートが閉じる。 - NES のアーケードシェル: それはどこに住むのか。そして
ntsc-wasmの 規約 (ドットの面を入れ、RGBA と歩調の統計を出す) は、その必要に合って いるか。 - 写真の往復 (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 から取り込まれる。写しはリポジトリの一つだけだ。報告は独自の作業用の言葉をいくつか使う: 報告書で使われる言葉。