6502tinymachines

コンソール規約

Die Runner はトランジスタレベルの MOS 6502 の上のコンソールだ。ゲームは 6502 の ROM、画面はそのチップのメモリの 1 ページ、描くのはブラウザ。エミュレータは無い: すべてのフレームが API を通じて、実物のダイの 3510 個のスイッチを整定させる。

コンソールはハードウェアではなく規約

このチップに映像は無く、ここでは割り込み線も使わない。だから「フレーム」はシリコンが知っている何かではない。ROM とそれを駆動するものとの間の合意であり、その合意がコンソールのすべてだ:

ホストがバイトをクリアする  ->  ROM が気づき、1 フレーム走り、立て直す
ホストがバイトを書く        ->  そのバイトがコントローラ
ホストがページを読む        ->  そのページが画面

ROM はフラグをビジーウェイトする。割り込みもタイマも無いとき、外界と同期する方法はそれしかない。これが HTTP 越しに動くのは、API がステートレスだからだ: フレーム境界は二つの /v1/step 呼び出しの間のメモリ編集で、マシン全体がそのそれぞれに載って移動する。

これのどこもゲームのために設計されていない。マシンを値として持ち運ぶ設計から、勝手にこぼれ落ちてきたものだ。

1 フレームの値段、実測

最初のフレーム (初期化: 256 セルのクリアと餌の配置)5,400 半サイクル
以後の毎フレームきっかり 600 半サイクル
それをチップ時間でおよそ 0.3 ms
エンジンへの往復およそ 200 ms

チップは三桁の差でボトルネックではない。 フレームレートは往復であり、 ページはそれを隠す代わりにそう述べる。

ビジーウェイトの代わりにフリーランするカートリッジなら、1 リクエストに 333 フレームを詰め込める。API の上限が 200,000 半サイクルだからだ。代償は入力の遅延になる。フラグのハンドシェイクは応答性を買い、その支払いとして 1 フレームに 1 リクエストを払っている。

タイル

8x8 ピクセル、1 ピクセル 2 ビット、1 タイル 16 バイト: NES の形だ。昔ながらのスプライトツールが吐くのがこれであり、タイルあたり 4 色という制約こそが、絵を写真ではなくあの時代に見せるものだからだ。

バイト 0..7    各ピクセルのビット 0、1 行 1 バイト、MSB が最左ピクセル
バイト 8..15   各ピクセルのビット 1
色             (plane1 << 1) | plane0   ->  0..3

パレットはダイ自身のもの、分解図がマスク層を塗る 4 色だ。それが Die Runner の趣向: 遊び場チップなのだ。

0#0B1120基板。何も載っていないダイ
1#3E93A6拡散層。スイッチされる層
2#E0A24Bポリシリコン。ゲートと、制御するものすべて
3#4FBFD4メタル。配線と、ランナーが乗るものすべて

色 0 はスキップされず描かれる: これはタイル画面であって、スプライト層ではない。

chr.js はコードで描かれたスターターセットを積んでいる。だからコンソールは絵が一枚も無いうちから描画でき、仕様は実行可能だ。ツールが何を作ろうと、正確にその形にデコードできなければならない。encodeCHR はその逆関数なので、アートのパイプラインとコンソールは、別々に漂う二つではなく一つの定義を共有する。

何も描かないカートリッジ

console.kind: "headless" は、画面ページも tick フラグも持たない、チップ上のプログラムだ。サイトには、カートリッジになったことのないプログラムが散らばっていた (エクスプローラが起動する七つ、API のページの例題)。鋳造も、掲載も、実測もされていなかった。いまは同じファイル、同じレジストリ、そして「見せるのはチップがしたこと」という同じ規則の下にある。

検証が語るのは、どこまで行ったかだ。レジストリはそれを起動し、console.half_cycles だけ走らせ、レジスタと、console.peek でカートリッジが名指したバイトをシリコンから読み出す。実行の最後の四分の一は四回サンプルされるので、終了時に pc がまだ動いていたかを報告できる。片側はループか終わったプログラム、もう片側は JAM だ。何も描かれず、一覧はそう言う (「何も描かない」)。持っていないフレームコストを見せることはない。

意味のあるレイアウト検査はそのまま効く。スタックページやベクタにかかる ROM は拒まれる。画面の検査は効かず、ファイルは画面のフィールドを持たないので、画面のないカートリッジから既定の画面アドレスを読む者はいない。コンソールはこれを起動しない。理由を添えて拒む。

ゲートは実物

各ゲートはこのダイに実在するスイッチであり、ゲームを走らせている当のチップの上で自分の制御線が high の時にだけ、正確にその時に導通する。何もクロック位相をシミュレートしない: 位相は、このコードを実行している 6502 がフレームの終わりにたまたましていることそのものだ。

ホストは 8 本の線を見張り、そのレベルを 1 バイトに詰め、ROM に手渡す。ゲートのセルは自分のゲート番号を運ぶ (16+g は線 g が high の間に導通するチャネル、24+g は low の間に導通するチャネル) ので、描かれるものと死なせるものが同じバイトから来る。画は、どちらが開いているかについて嘘をつけない。二つのチャネルは相補なので、通り道は常にある。それは優しさではない。パストランジスタとはそういうものだ。

8 本は好みではなく実測で選ばれた: 名前付きノード二つの間のスイッチをゲートする線を、実際のプレイ 24 フレームでどれだけ動いたかで順位付けしたものだ。動かない線のゲートは常に閉か常に開で、それは背景でしかない。

ゲート制御線high反転ゲートするスイッチ
0dpc25_SBDB16/2410sb0 - idb0
1dpc9_DBADD18/249idb0 - alub0
2dpc10_ADLADD6/249adl0 - alub0
3dpc21_ADDADL3/246alu2 - adl2
4dpc23_SBAC4/244sb0 - a0
5dpc30_ADHPCH21/244pch3 - adh3
6dpc40_ADLPCL21/244adl0 - pcl0
7dpc2_XSB2/242x0 - sb0

このラベルは手書きのものだ。いまはアトラスが導出していて、この表の x0 - sb0 を含む三つで意見が違う。

ゲート 5 と 6 は一緒に動き、これからもそうだ: ADHPCHADLPCL はプログラムカウンタ自身の往復であり、すべてのオペコードフェッチで発火する。実は一つの事象である二つのゲート、というのはチップについての真実なので、両方とも残してある。

サンプリングは 1 フレーム遅れで、そうであるほかない: 読むものができるには、チップが先に走っていなければならない。だからあなたが縫っているゲートは、いま見ているフレームを描き終えた時点の CPU の状態だ。

設計ではなく実測が必要だった二つのこと

  • ランナーは行 13 ではなく行 2 に座る。 新しい地形は行 15 に現れて上に スクロールするので、行 13 からだと壁は出現の 2 フレーム後に着く: 毎秒 5 フレームでは警告が皆無だ。行 2 からなら同じ壁が 13 フレーム先にあり、待ち伏せされるのではなく、ダイの中へ降りていくように読める。
  • 隙間は漂う。どこかに着地はしない。 ランダムな列の隙間は、柵が届く前に ランナーが歩ける距離より遠いことがある。それは難しさではなく、プレイヤーに避けようの無かった死だ。各隙間は移動 6 フレームに対して、前回から -3 から +4 だけ動く。

画面は $0500 に引っ越した

背景を足したら ROM は 359 バイトから 521 バイトになり、$0400 を越えた。そこは自分の画面が住んでいた場所だ。アセンブルは通り、ブートも通り、画がコードを食う。games/tools/asm.mjs --limit $0500 はそれを謎ではなくビルド失敗にし、画面は 1 ページ上に座る。

引っ越しはアドレス 4 箇所で、1 箇所の見落としは無音だ$0410 はスクロールのコピー元で、置き去りにすると、ゲームは毎フレーム無関係のメモリを画面に写し、ほとんど空のダイを描いた。何もエラーにならなかった。

規約がカートリッジの横ではなく中を旅する理由がこれだ。 カートリッジを参照。