6502tinymachines

窓の中のコンソール

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

走行スタンプ: 2026-09-06、rustc 1.97.1、wgpu 22.1、winit 0.30、cpal 0.15、node 24 の下の wasm-pack 0.13。ピン留めは N7 と同じ、ntsc-crt v0.2.4。cargo test -p nes-shell: 3 テスト (GPU の絵、歩調を合わせたループ、リング)、追記のパッドのテストを入れて 8。計画は docs/n8-plan.md、先に書かれた。

コンソールにシェルができた。絵を窓に出す Linux のバイナリで、復号と CRT の各段は GPU に載り、コンソールは自分のスレッドで壁時計に歩調を合わせ、音は音声デバイスから出て、キーボードがコントローラ 1 になる。そして音を持つコンソールの wasm ビルドを node の下で測った。窓はここでは、箱の二枚の RTX 3070 の一枚の上で仮想ディスプレイの中を走った。画面とスピーカーと手のある机が、残っているものだ。

一フレームの時間はどこへ行っていたか、そしていまどこへ行くか

picture-bench (一コア、full_palette.nes、この箱で無関係な負荷平均 8 の下): コンソールが 1 フレーム 7.1 ms、音付きでは節目の初めに 13.0、終わりに 9.3 (音の DAC を表にし、再標本化器を連続したバッファに載せて標本ごとの算術を無くした: 5.9 だったところが 1.7 ms)。符号化器が 5.0。3 ライン・コムの復号が 28。CRT の各段が 55。周期は 16.64 ms。復号と各段が、どのコアにも収まらなかった 83 ms だ。それがいま GPU でおよそ 1 ms、アップロードを含めて。

手順 1: GPU の上の絵

nes_shell::gpu::GpuPicture: ストレージバッファ上の八つの計算パス (picture.wgsl) で、定数はすべて打ち込む代わりに Decoder と CrtParams のインスタンスからアップロードする。コムのクロマを各行の位相で復調し、間引き率でブロック平均する。間引かれたローパスは端を複製する。輝度のスケール、Catmull-Rom でグリッドへ戻し、行列、クランプ、表示ガンマ。ビームのガウシアンの列。走査線は CPU の散らし版に対する集め版で、同じ 1e-4 の打ち切りと同じ総和の順序。状態バッファに対する残光。マスク。バイリニア標本化を伴うジオメトリ。窓は最後のバッファをガンマ符号化して blit する (blit.wgsl)。

検査 (tests/gpu.rs): コンソールフレーム二つとその後の黒フレーム一つ (保持されている絵が見える場所) を Decoder::decode と CrtPipeline::process に通し、またパスにも通す。書き下ろしのパラメータで一度、マスクとジオメトリをオンにしてもう一度。許容は計画から: すべての成分が線形の光で 1e-3 以内、平均は 1e-5 以内。

最悪の成分平均1 フレームの GPU 時間
書き下ろしのパラメータ4.8e-71.2e-81.02 ms
マスクとジオメトリをオン2.8e-51.9e-71.08 ms

二つの f32 の連鎖が最も大きく分かれるのは、ジオメトリが小数位置でバイリニア標本を取るところで、それでも許容の百分の一の内側だ。MUTATE=1 は残光を飛ばし、黒フレームは両方のワールドで 0.53 の赤になる。アダプタが無ければ名指しで SKIP し、REQUIRE_GPU=1 が強制する。

手順 2: シェル

nes-shell rom.nes。コンソールのスレッドがコンソールを組み立て (チップ群は Rc を通して状態を共有するので、走る場所で組み立てる)、周期ごとに ntsc-wasm の Pacing が壁時計から見て到来していると言うぶんを走らせ (二周期かかった一拍は二フレーム走らせて欠落を一つ数え、ゼロ周期の一拍は前のフレームをもう一度出して重複を一つ数える)、最新のフレームを公開し、音を 4 分の 1 秒で上限を切ったリングに貯め、周期の残りを眠る。表示のスレッドは自分の時計から同じ周期で歩調を取るので、vsync の無いサーフェスでも暴走できない。それが新しいフレームごとに CPU で符号化し、GPU の絵を走らせて blit する。cpal が 48 kHz でリングを汲み出す。キーボードが、コンソールのスレッドが毎フレーム読むパッドを設定する。終了時に計数器が印字される。NES_SHELL_TICKS=n は n 回の再描画の後に終了する。

検査 (tests/pacing.rs、窓なし): 合成した時計の上のループ。周期どおりなら 60 拍で 59 フレームが走り重複が一つ (周期はナノ秒の整数倍ではなく、最初の一拍が 1 ナノ秒足りない)。周期の半分では一拍おきに重複し、二倍では一拍ごとに一つ欠落する。1 フレームは 798.7 標本を貯め、リングは 12,000 を保ち、それを越える汲み出しはアンダーランとして数える。

窓を、この箱の Xvfb の下で、負荷平均 8 のまま full_palette.nes を表示側で 600 回再描画した: コンソールのスレッドの 1 フレームは平均 11.4 ms、最悪 82 ms、1,467 のうち 134 が 16 ms 超、2 が 33 ms 超。コンソールの 1,394 周期で欠落 74、音声のアンダーラン標本 4,800 (ここには音声デバイスが存在しない。リングの計数器はシェル自身の煙出し走行 (smoke run) がする汲み出しだ)。スレッドはそれぞれ自分のぶんを数え、NES_SHELL_TICKS は表示の 600 回目の再描画で走行を終える。だから上の周期とフレームは、同じセッションにおけるコンソールのスレッド自身の計数だ。記録であって、押さえではない: 箱は暇ではなく、ディスプレイは本物ではなかった。

手順 3: 二つ目の標的

cargo check --target wasm32-unknown-unknown -p nes-console が通る: コンソール、両方のラング、ntsc-crt がブラウザ向けにコンパイルできる。チップの表がビルド時にホスト側で作られるからだ。nes-wasm が橋で (Nes::new(rom)、run_frames、ntsc-wasm のパイプラインが取る色と強調の面とパリティ、汲み出された音、1 バイトとしてのパッド)、tools/wasm-bench.mjs を node の下で: full_palette.nes を音ありで 300 フレーム、3.29 s、毎秒 91.1 フレーム、実時間の 1.52 倍、1 フレームあたり音の標本 798.7。それを載せるページは屋根の項目だ。

机に残るもの

  • 本物のディスプレイの上の窓: 絵、セッションの終わりの歩調合わせの計数器、走査線と残光がスケール 3 で正しく見えるか。
  • スピーカー: 一セッションを通してリングのアンダーランの計数器がゼロであること。
  • キーボードの上の手、そして検査 3 のカートリッジ。そこは N5 の遊びの検査が閉じる場所でもある。
  • 屋根に載る wasm のページ (その後できた: /nes/play)。
  • 手に持つゲームパッド: 下の対応付けは一つも無いまま走った。

追記、2026-09-06: キーボードの隣のゲームパッド

nes_shell::pad (gilrs 0.11) が再描画ごとにパッドのイベントを汲み出して Buttons にし、窓がそれをコンソールのスレッドが読む前にキーボードのものと OR する。配置は NES のパッドに対して位置で決める: 東と北の面ボタンが A、南と西が B、Start と Select と十字キーは名前どおり、左スティックはどちらの軸でも半分を越えた傾きを方向とする (行きも帰りも同じ境目で、境目を越えていないスティックは何も書かないので、押されたままの十字キーを打ち消すことはできない)。パッドが無いこと、列挙器が無いことは誤りではない: キーボードで遊べるままで、理由が一度だけ印字される。検査 (tests/pad.rs、5 テスト、パッドなし): 何にも対応しないものを含む配置の表、両方の軸で行きも帰りもスティックの境目、右スティックとトリガが無視されること、スティックの雑音を生き延びる十字キー、OR、そしてこの箱での Pad::open (列挙器は開き、パッドが無いのでイベントは来ない)。仮想ディスプレイの下の窓の煙出し走行は、このモジュールを入れても変わらない: 90 回の再描画、88 の新しいフレーム、アンダーラン標本 0、ゲームパッド 0。本物のパッドがどう感じるか、東が A でよいのかは親指の選ぶことで、机のものだ。

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