6502tinymachines

模型の中の実物のカートリッジ

2026-09-12 に追加。本物のゲームでコンソールと模型を比べるには、模型が同じ バイト列を走らせなければならない。つまり、ベンチにあるカートリッジをダンプし、 そのダンプを検証し、そのカートリッジが使う基板をコンソールに教えるということだ。 このページはその道筋と、それが開いた三枚目の絵である: タイトル画面の模型の フレームを、ベンチがすでに持っていた二枚、スコープの復号とグラバーのものの 隣に。

リーダー

ベンチのカートリッジリーダーは OSCR (Open Source Cartridge Reader、HW5) で、 ファームウェア V15.6 を焼いて Pi の /dev/ttyUSB0 につないでいる。操作はその 画面とダイヤルで、シリアルの経路はファームウェアの更新専用だ。だからダンプは ダイヤルに置いた手であり、ファイルは SD カードで出ていく。

リーダー、そのスロットとメニュー

カートリッジは Super Mario Bros. と Duck Hunt が一つの基板に載ったもので、絵の 試験のときコンソールに入っていたゲームだ。リーダーは最初に間違った推測をし、 その推測のしかたは書き留める価値がある。リーダーはカートリッジを、ROM 全体 ではなく、二つの決まったアドレスで読んだ 512 バイトのチェックサムから同定する。 この基板ではその窓が素の Super Mario Bros. に一致したので、リーダーはその項目を 提示し、そのゲームのサイズ、マッパー 0、プログラム 32 KB、キャラクタ 8 KB で 読んだ。結果のチェックサムは何にも一致しなかった: 二バンクの基板の片方の バンクを丸ごと読んだのであり、そのバンクを単独で持つ市販カートリッジはない (その最初の 9 バイトは、ゲームのリセットがあった場所に立つ基板自身のバンク 切り替えだ。残りは基板の報告にある)。"CRC not found" は、壊れたカートリッジ ではなく一バンクだけのイメージについて、リーダーが正直だったということだ。

カードと、合ったダンプ

本当でなければならないことが二つあった。まずリーダーのデータベースがこの カートリッジを知っていること: カードにはまだ古いファームウェアのデータベースが 入っていたので、Pi から更新した (card-plan.md。Pi につないだ USB のカード リーダー、45 個のファイルを写して読み返して同一、nes.txt は 9,330 行から 22,143 行へ)。次に、読みが正しい基板を使うこと。リーダーの上で Change Mapper を 66 に、プログラム 64 KB、キャラクタ 16 KB にし、それから Read PRG/CHR ではなく Read iNES Rom を選ぶ。そうすればリーダーがプログラムの両バンクとキャラクタの 両バンクを自分で切り替え、ヘッダ付きの .nes ファイルを書く。

そのダンプは 81,936 バイトで、そのチェックサムはデータベース自身のものだ:

CRC32 (本体)D26EFD78
データベース上の名前Super Mario Bros. + Duck Hunt (USA).nes
マッパー66 (MHROM)
プログラム4 x 16 KB = 64 KB
キャラクタ2 x 8 KB = 16 KB

名前の付いたカートリッジは、チェックサムが一致するまでは確かめられたカート リッジではない。これは一致する。カードからワークステーションの ROM 置き場 (リポジトリには決して入れない) に移し、上のすべてを記録したマニフェストを 添えた。

模型が育てねばならなかった基板

コンソールの模型が知っていたカートリッジの基板は一つ、NROM (マッパー 0) だけ だった。プログラムもキャラクタもバスに直結で、切り替わるものは何も無い。この カートリッジはマッパー 66、GxROM だ: 書き込み専用のレジスタ一つが、バスに 見えるプログラムの 32 KB バンクとキャラクタの 8 KB バンクを選ぶ。そこで模型は その基板を得た (nes-bus が Gxrom を得た)。基板の記述ではなく基板そのもので あるようにする細部が一つある: この基板はバス衝突への保護を持たないので、 レジスタへの書き込みは、それが着地するアドレスの ROM のバイトと AND される。 シリコンがやるとおりに、だ。だからゲームはバンク番号を、その番号をすでに ROM が 持っているアドレスに書く。この変更は一族のクレートをバージョンで渡っていき、 コンソールと絵の符号化器がいまも一つのフレーム型を共有し、どのリポジトリの テストも緑のままだった。

一つのタイトルの三枚の絵

ダンプが模型に入り、その下に基板が入ったので、コンソールのタイトル画面は三通り 存在する: 一族自身の復号器で復号したスコープの記録、グラバーのフレーム、そして いまは同じ復号の連鎖を通した模型自身の描画だ。

タイトル画面での模型と復号器

二つの目は、前のページが見つけたとおり一致する: この静止したタイトルの上で、 平坦なブロックは 255 のうち 0.57、色相は 0.7 度。模型をそれらに対して測るのが 新しい測定だ:

平坦なブロック、平均絶対差色相の中央値、彩度の高い画素輝度の相関
模型 対 スコープの復号255 のうち 0.6712.6 度0.95
模型 対 グラバー255 のうち 1.2614.1 度0.95

平坦なブロックが一致するのは、タイトル画面の大半が黒だからだ。彩度の高い色は 一致しない。ロゴの茶色は模型で (148, 92, 0)、スコープ側で (132, 73, 0)、 グラバー側で (121, 69, 0)。文字のシアンは模型で (59, 200, 251) に対して、およそ (45, 200, 205) と (67, 202, 202)。一つのコンポジット信号に対する二つの別々の 復号器が色相 1 度で一致するところで、模型は両方から 12 度から 14 度離れて座る。 シアンではより青く、茶色ではより暖かい。

三面の絵の全体と、模型の絵の連鎖のどの段が仲間外れなのかという推論は、「目 対 スコープ」のページにある。CPU とスプライト DMA を直して (下の節) 2026-09-13 に そこで再走行すると、色相の数字は小数第二位まで同じで戻り、迷子のスプライトは まったく戻ってこなかった。

このページが記録するのは、三枚目の絵がどうして可能になったかだ: カートリッジを正しく読み、リーダー自身が鍵にしているチェックサムに 対して検証し、コンソールが走らせるバイト列を模型が走らせられるように基板を 足した。それが出した測定、二人の証人を立てて実機に対して測った模型の色相は、 ベンチが存在する目的の仕事をしているところだ。

最初のトレースと、それが見つけたタイル

カートリッジが模型に入ったので、計画の次の段は、コンソールの走行を 6502 の スタックが読めるファイルにすることだった: ピンのクレート自身の形式で半サイクル ごとの CPU のピン、刺激ファイルとしての入力ピン、そしてピンには見えないが コンソールが知っていることのためのイベントファイル (フレームの終わり、パッドの ラッチすべてとそのバイトと読み出し回数、PPU のレジスタ書き込みすべてとその ドットとライン、ROM 空間へのあらゆる書き込み、NMI のあらゆるエッジ)。道具は コンソールの trace の例で、このサイトにあるトレースの計画がそのゲートと 走らせ方を記録している。

一族のカートリッジでの最初の走行は 300 フレームで、200 番目のラッチで Start を 押した。イベントファイルは、マルチカートのメニューがアニメーションのために CHR バンクを 1 フレームに二度ひっくり返すのを、Start での Super Mario Bros. の プログラムへのバンク切り替えを、そしてスプライト DMA がピンの上でどう見えるかを 見せた: $4014 への書き込みのたびに $2004 への書き込みが 256 回。最後のフレームは タイトル画面で、タイルが一つ間違っていた。

タイルが一つ間違った模型のタイトル画面

1 PLAYER GWME。イベントファイルはその書き込みの場所を特定した: そのタイルの ために $2007 へ送られたバイトは $20、文字 W だったが、ROM の文字列は、その少し 前に CHR-ROM から $2007 を通して読まれたとき $0A、文字 A を運んでいた。ピンが それを遡った: そのバイトは LDA ($00),Y から来ていて、そのポインタとインデックス はページを跨いでいたのに、CPU は桁上げ前のアドレス $0300 を読み、A が置かれて いた $0400 での修正後の読みを一度もしなかった。5 つの命令が、6502 リポジトリの スイッチレベルのチップと高速ラングを並べたロックステップの上でそれを再現した: INY の後、高速ラングは次の命令の変種を、格納された Y に訊いて選んでいた。一 命令ぶん古い Y だ。インクリメントの結果はまだ ALU のホールドレジスタにあり、 着地するのは半サイクル後なのだ。スイッチレベルのチップにはそんな問いが無い。 桁上げは正しい Y でトランジスタから落ちてくる。

ラダー自身のオラクルのどれも、この並びを持っていなかった: ピンのゴールデンの オペコードのトレースはレジスタをロードで設定し、命令のテスト ROM は インデックスのインクリメントとページを跨ぐ形を一度も組にしない。修正は 6502 リポジトリにあり、レジスタを設定してから跨ぐ対を両方向に 22 組、スイッチレベルの チップに押さえ、格納されたレジスタに訊く妨害を付けて、効く 9 つで赤になる。 その修正の最初の版は片側だけのテストを通り (その中のどの場合もページを跨いで いた)、このカートリッジのメニューをフレーム 9 で詰まらせた。同じトレースを二つの 版で走らせて 1 行ずつ差分を取ると半サイクルを名指しし、5 つの命令がそれも再現 した。二つ目の版で組み直すと、同じ走行はこう読める:

修正後の同じフレーム

トレースはそのためにある。商用のカートリッジは、どんな文脈も書かれていない プログラムであり、その最初の一本が、あらゆるノードと同じ幅のゴールデンでも 見えなかった高速ラングの欠陥を見つけた。ゴールデンは、それが走らせた プログラムの幅しか持たないからだ。

証人としてのダイ

トレースの次の段は、その記録を 6502 リポジトリのスイッチレベルの 6502 に流し 戻すことだった: あらゆる読みを記録自身から答え、あらゆる書き込みをそこに保持 するバス、残りはトランジスタがやり、両者をピンで半サイクルごとに比べる。一族の テストカートリッジでは、ダイは記録の 12 フレームすべてにわたってコンソールと 一致した。このカートリッジでは、最初のスプライト DMA が CPU を解放するまで一致し、 その差は、記録が 2A03 のピンが見せるとおりのホールドを運んでいたことによるもの だった。2A03 は自分のコアにそれを 1 サイクル遅れて渡す。コア自身のレベルでなら、 ダイは最後まで一致した。

それからコンソールは、成立した分岐を高速 CPU がダイと同じように読むように直した 上で、このカートリッジのメニューで詰まった: NMI が切られ、主ループは決して 来ないスプライト 0 のヒットを待っていた。その記録の上でダイは最後の半サイクル までコンソールと一致し、それが CPU を疑いの外に置いた。そして記録の DMA が 運んだバイトに対してワーク RAM をダンプしたことが原因を名指しした: 2A03 のラング のスプライト DMA は、保持された CPU のバスを通して読んでいて、そのバスのメモ化は CPU の静かな問い直しに答えるので、DMA が以前に読んだページはそのときのままの姿で 返ってきた。スプライトのバッファを一度も読まないゲームは、最初のフレームの スプライトをいつまでも持ち続けたのだ。バス上の二つの DMA のテストを付けて直すと、 タイトル画面ではマリオが地面に立ち、先の三方向の比較が下線の上に見ていた小さな スプライト、どちらの目も持っていなかったものは消えた: それがこれだった。

一晩で三つの欠陥、どれもトランジスタの中ではなく、どれも、知らないことを埋めよう としない模型の上で本物のプログラムを走らせたから見つかった。

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