Tachibana

Astéroric › 記事 › Asteroids を Oric-1 に移植する:抵抗したもの

Asteroids を Oric-1 に移植する:抵抗したもの

1979 年のベクターゲームを 1 フレーム 20,000 サイクルに収めるまでの 40 フェーズ。譲ったもの、抵抗したもの、そしてエミュレータが私たちに隠していたもの。

著者 bmarty · Astéroric 1.0.0-beta · 2026 年 9 月 14 日

Asteroids はベクター画面の筐体で動きます。線分を描くビームがあるだけで、ピクセルもフレームメモリもありません。Oric-1 はその正反対です。240 × 200 ドットのビットマップ、1 MHz の 6502、一部を画面に取られる 48 KB、そして矩形波 3 チャンネルのサウンドチップ。Astéroric プロジェクトは研究クローンです。rev 4 ROM に書かれているままのアーケードロジックを取り(同じプロセッサなのが助けになります)、このマシンに収めること。この記事は、そのとき抵抗したものについて書いています。フェーズ番号はリポジトリの CHANGELOG を指し、そちらが正です。

1. 成り立たなかった予算

最初の計画は算数から始まりました。1 MHz で毎秒 50 フレームなら、1 フレームあたり 20,000 サイクルです。自己書き換えコードなしの正しい 6502 ブレゼンハムルーチンは 1 ピクセルあたり 15〜22 サイクルかかります。20 ピクセルの線分を 30 本、描いて消せば、何も計算しないうちに予算は尽きます。文書は 3 つの選択肢を挙げていました。25 Hz を受け入れる、形状を単純化する、あるいは線分ルーチンに徹底的に投資する。

フェーズ 2 の測定が決着をつけました。1 ピクセルあたり約 97 サイクル、仮定の 5 倍です。25 Hz は即座に採用されました。劣化ではなく名目上の目標として、そして多くの Oric ゲームがそのフレームレートです。フェーズ 2b の自己書き換えコード(dx/dy オペランドを即値にパッチ、Y ステップの向きを入口でパッチ)は 1 ピクセルあたり 4 サイクルしか稼げず、目標の 50 には遠く及びませんでした。本当の節約は後になって、別の場所から来ました。

2. ビットマップにベクターを描く

すべては XOR で描かれます。線分をもう一度描けば消え、背景を保存する必要はありません。エレガントですが、フェーズ 9g で見つかった即座の欠点があります。2 本の線分が共有するピクセル — 自機の先端、閉じた多角形のすべての頂点 — は 2 回 XOR され、つまり消えます。小惑星の頂点はすべて見えませんでした。最初の修正は共有頂点の明示的な再描画でした(3 回の XOR = 点灯)。正しい修正はフェーズ 24 で来ました。]P0, P1]、つまり始点を除くすべてのピクセルを描く半開ブレゼンハムです。閉路として辿る多角形では、各頂点はちょうど 1 回だけ XOR されます。正味コストはゼロ — 線分ごとに 1 ピクセル減る分が入口の判定を賄います。

決定的な性能向上は線分ルーチンからではなく、それをもう呼ばないという発想から来ました。小惑星は決して回転しません。12 のシルエット(4 形状 × 3 サイズ)は、line.s のブレゼンハムを再現する Python スクリプトでビルド時にラスタライズされ、バイト内の 6 つの位相に事前シフトした HIRES ビットマップとして出力されます(3,402 バイトのデータ)。描画はバイト単位の XOR ブリットになり、非ゼロのバイトあたり約 23 サイクル。効いてくる細部:240 は 6 で割り切れるので、折り返しの幽霊インスタンス(±240 px)は同じ位相を保ちます — ビットマップは 1 セットで済みます。フレームレートを保つためにフェーズ 18h で強いられた 6〜7 頂点への間引きは撤廃できました。コストが頂点数に依存しなくなったので、Atari の形状の細かい切れ込みが戻ってきたのです。

ついでにプロファイラでの驚き:sizeof(Asteroid) = 13。2 のべき乗ではないため、asteroids[i].field へのアクセスのたびに cc65 はソフトウェア乗算 mul8x16 を呼んでいました — CPU 時間の 7.1 %。ポインタで反復するとプロファイルから消えました。合計で、フェーズ 24〜26 は CPU の空き時間を 17.9 % から 39.8 % に引き上げました。

3. 時間:存在しない VSync

フェーズ 9 は、ULA の VSync 信号を受けているはずの VIA の CB1 ピンでループを同期していました。ゲームは Phosphoric 上で完璧に動きました。その後 Phosphoric 1.16.11 がハードウェアに合わせたところ、ゲームはタイトル画面で無限ループするようになりました。実機の Oric では CB1 は VSync に配線されていないのです。エミュレータは便宜上ピンをパルスさせ、誤りを覆い隠していました。代替は 20 ms でフリーランする VIA の Timer 1 — 実機、Phosphoric、Oricutron、MAME に移植可能で、理論上のティアリングは一度も観察されていません。

より微妙なタイミングのバグが残っていました。フェーズ 18 以来「混雑したシーンで実効 17 Hz」という名で知られていたものです。frame_wait() は入口でカウンタを読み、そこから 2 ティック待っていました。作業が最初のティックに収まるうちはタイマーの格子が 1 フレーム 40 ms を与えますが、フレームが 20 ms を超えた途端、そこから丸々 2 ティックを待ち直す — 40 ms ではなく 60 ms。フェーズ 28 は永続的な目標(frame_target += 2)を持つ固定ステップスケジューラを導入し、後続フレームで追いつき、長いフリーズの後には再同期します。適応 50 Hz は検討の上で却下されました。物理はすべてフレームあたりのピクセルで、約 15 系統のタイマーがフレーム単位で数え、UFO は ±1 ピクセル動きます — 時折 50 Hz に切り替えれば、すべてを再スケールしない限りゲームの速度が倍になります。一定で信頼できる 25 Hz は、断続的で危険な 50 Hz に勝ります。

4. ちらつき

25 Hz では、フレームの先頭で消され末尾で描き直されるオブジェクトは、時間の約 80 % 画面から消えています。目は最悪の周波数で出現/消失のサイクルを見ることになります。修正は構造的でした — エンティティごとに erase → tick → draw を密着させ、描画でないもの(サウンド、スコア、テキスト)をすべてその窓の外へ出す。小惑星の場合、窓は約 20 ms から 7 ms になりました。

魚雷はもっと厄介な変種を見せました。実機のテスターが指摘したものです(「fire beam hardly visible」)。古いパイプラインはループの先頭で消し、末尾で描き直していました。計算のほぼ全期間、ビデオメモリに存在しないのです。実機では CRT の走査と Timer 1 の位相がドリフトするので断続的に現れましたが、Phosphoric では位相がロックされ、20 回のキャプチャのうち魚雷が見えたのは 0 回 — RAM は有効だと言っているのに。コミット方式(最後に描いた位置で消し、直後に描き直し、画面状態を論理状態から分離)で 8 回中 8 回になりました。魚雷は 2 × 2 px にもなりました。

5. 迂回する ROM

キーボードは ROM のバッファを使わず、マトリクスから直接読みます — マトリクスを理解していることが前提です。最初のバージョンは間違っていました。コードは固定の R14 マスクのまま異なる列を調べていました。ハードウェアでは SPACE と 4 つの矢印キーはすべて列 4 にあり、PSG のレジスタ 14 で選ぶ行で区別されます。右 SHIFT だけが誤検出を生んでいました。第二の罠は 6522 固有のもの:ORB を読むと、出力に設定されたビットについては書き込んだ値が返り、ピンの状態は返りません。ROM が DDRB = $FF のままにしていれば、PB3 は自分の値を読み返し、スキャンはランダムになります — 撃ち損ないと一貫しない回転の原因です。key_scan は今ではスキャンの間 PB3 を入力に強制します。

第三の罠は、理解する前に耳で聞こえていました。PSG のポート A をマトリクスモードで駆動するため、key_scan は毎フレーム R7 = $7F —「ミキサー全消音」— を書いていました。6 つのトーンとノイズのビットがフレームごとに 1 回切られ、次の偶数サウンドティックまで切れたまま:すべての効果音が 25 Hz で刻まれる、長い音ほど耳につく現象です。それがバックログの「自機爆発のオーディオバグ」でした。修正は mixer_shadow に保持したミキサーの最後の実際の値を書き戻します。

割り込み駆動のサウンドプレーヤー(フェーズ 20)では、ROM がユーザーベクターへジャンプする前に何をするかを知る必要がありました。答えは何もしない。6502 は PC と P しかプッシュしません。A、X、Y の退避は ROM ハンドラの内部で行われ、ベクターをパッチすることでそれを短絡してしまいます。自前の PHA/TXA/TYA がなければ、最後の 3 連 PLA が P と戻りアドレスをポップし、RTI はどこへでも飛びます。T1 を有効にする前に VIA の他のすべての IRQ ソースも無効にしなければなりません。さもないと ROM の残した CA1/CB1/T2 が、決して応答しないハンドラを起動します — 無限ループ。Phosphoric の --trace-irq で検証:493 回の進入、493 回の RTI、IRQ 間は 19,930〜19,970 サイクル。

ハンドラは両方のベクター、$0228(Oric-1)と $0244(Atmos)にフックされています。それでも足りませんでした。Atmos ROM ではゲームは動きません。Phosphoric でも Oricutron でも。Atmos 互換は別スプリントのままで、サイトもそう書いています。

6. BSS、二度

C は静的変数が起動時にゼロであると仮定します。それを保証するのは crt0 の役目です。フェーズ 9b:BSS が $83 バイトを超えると HIRES 画面が有効にならなくなりました — 自作の zerobss は cc65 のものと微妙に異なっており、解決策は initlib 経由で公式ルーチンを採用することでした。フェーズ 36、その 1 か月後:最初の発射で座標 (85, 85) に幽霊ブロックが現れます。85 は $55、未初期化 RAM のパターンです。「initlibzerobss を呼ぶ」というコメントは誤りで — cc65 はそれらを別々に呼びます — BSS は実は依然としてクリアされていませんでした。2 回のコードレビューは、コメントを読み直していなかったのです。

7. 別のマシンのサウンド

1979 年の筐体は離散アナログ発振器で音を作っています。移植するものは何もありません。出発点は別の 8 ビット機でした。Vectrex の Asteroids クローン、Mine Storm(1982)です。プロセッサは違いますが(6809)、同じ AY-3-8912 チップと同じ VIA 6522 — コードは移せなくても、レジスタ値は移せます。SS.THRSS.BLTSS.EXP のテーブルが最初の近似になりました。

残りは耳とスペクトルで行いました。筐体の録音の FFT 解析です。3 種の爆発(小・中・大の小惑星)は持続時間が似ていて、ノイズ周波数だけが異なります — 約 167、246、306 Hz、つまり R6 = 12、8、6。発射音は低いノイズから約 740 Hz になりました。アーキテクチャは筐体に倣いました。3 つの独立した回路が 3 つの専用チャンネルになります — A は効果音、B は爆発に決して中断されないドンドン、C は円盤 — ミキサーは 3 つの状態から再計算されます。タイトルジングル(グリーグ、パブリックドメイン)は 3 フェーズの間聞こえませんでした。ルーチンは音符のインデックスを sound_tmp に置いてから psg_write を呼んでいましたが… その psg_writesound_tmp をスクラッチとして使っていたのです。読み戻された音符は PSG に最後に書いた値でした。専用のスクラッチで十分でしたが、ゼロページが 1 バイトの余裕もなく満杯だったため BSS に置かれました。

8. 嘘をつくエミュレータ、答えるハードウェア

Astéroric は Phosphoric の上で開発され、2 つのプロジェクトは互いを修正しました。エミュレータは存在しない VSync を隠し(§3)、次に誤った IJK モデルを隠しました。最初のジョイスティック読み取りは Phosphoric がシミュレートしていた通り PSG のレジスタ 14 経由で、テスターの物理インターフェースでは動きませんでした。本当のプロトコルはプリンタポート経由 — VIA のポート A 直接、PB4 ストローブ、存在ビット 5 — で、Oricutron に対して検証されました。Phosphoric も並行して修正され、そのテストにはゲームの正確な 6502 シーケンスの再生が含まれています。

ハードウェアは、どのエミュレータにも見せられないものも明らかにしました。初期化時に書くビデオ属性 $1C60 Hz の HIRES を選びます。ビット 1 が 50 Hz を選ぶので、$1E が必要です。ビデオメモリの内容はどちらでも同じなのでスクリーンショットも同じ — しかしテスターの RGB2HDMI は信号にロックしませんでした。ゲーム速度への影響はありません。走査ではなく Timer 1 で刻んでいるからです。逆方向では、エミュレータが計器を提供しました。ビット単位で比較するヘッドレスキャプチャ(make check)、2,500 万サイクルのプロファイル、IRQ トレース、シンボル化した RAM ダンプ、スクリプト化した --type-keys シナリオ。ただし罠つきで:スクリプト化したキー押下は 40 ms、25 Hz でちょうど 1 フレーム続くため、決定論的実行では 2 回のキーボードスキャンの間に系統的に落ちることがあります。

9. 残っているもの

出典とリンク

← Astéroric · 機能 →