Tachibana

Neo6502 › 記事 › AsteroNeo

移植:Astéroric から AsteroNeo へ

同じゲーム、同じ C、別のマシン:1 MHz の 6502 と自作ビットマップから、RP2040 が線を引く 6.25 MHz の 65C02 へ移るとき、何が変わるのか。

著者 bmarty · Tachibana · 2026 年 9 月 16 日

Astéroric は、48 KB の Oric-1 向けに作った Asteroids(Atari、1979)の研究クローンです。アーケード ROM rev 4 のロジックを cc65 の C で書き直し、自作の XOR ベクター描画で、1 フレーム 20,000 サイクルの中で 25 Hz — そこに至る 40 のフェーズは前の記事に書きました。AsteroNeo はその Olimex Neo6502 への移植です:6.25 MHz の本物の W65C02S、チップセット役の RP2040、$FF00 の API。方針:専用リポジトリ、上流ファームウェアのモード 0(320×240、256 色)、全画面のフィールド、ロジックは cc65 の C・API 層は ca65、30 Hz、API による XOR 描画、テストのオラクルは Phosphoneo、スモークテストは公式エミュレータ neo。2 スプリント後、ゲームはエミュレーション上で遊べるようになりました。何が変わり、何が抵抗したかを記します。

Oric-1 → Neo6502 を一表に

Oric-1(Astéroric)Neo6502(AsteroNeo)
HIRES 240×200 モノクロ、自作 XOR Bresenhamモード 0 320×240、API の XOR 線とピクセル(5,1 / 5,2 / 5,5)
小惑星はプリレンダしたビットマップ(ブリット)アーケードの形状から導出して滑らかにした多角形(24/16/10 頂点)、RP2040 が描画
キーボード:VIA/PSG マトリクスHID コードによる Key Status(1,2)、5 つのアクションは再割り当て可能
Timer 1 IRQ 下の AY-3-8912矩形波/ノイズ 4 チャンネル生成器、ノートキュー(8,7)
VSync/Timer 1、25 Hz60 Hz フレームカウンタ(5,37)、ゲームステップ 30 Hz
8 ビット座標int 座標(320 > 255)、桁上げ付き 8.8 積分

1. 動かないもの

ゲームロジック — ウェーブ、小惑星の分裂、2 種の円盤の AI、ハイパースペース、ハイスコア — はそのままです。アーケードの逆アセンブルを転写したもので、ハードウェアに依存しません。Astéroric の 7 つの C モジュール(gameasteroidsufohudfonttitlekeys)はそのまま別のマシンへ移ります。Oric のハードウェアに触れていたもの — HIRES、VIA、AY — はすべて 4 ファイルの Neo6502 層に置き換わります:neo_gfx.s(描画)、neo_time.c(タイミング)、neo_input.c(キーボード)、neo_sound.c(サウンド)。

2. 描画:自作 Bresenham からファームウェアの線へ

Oric では線は 1 ピクセルあたり約 97 サイクルかかり、それが 25 Hz を決めました。Neo6502 では線を引くのは RP2040 です。65C02 は座標を置いて関数 5,2 を呼ぶだけ。本物のファームウェア上の共シミュレーション(Phosphoneo + libemul)で測ると、線 1 本は約 22 65C02 サイクル(ARM 544 ステップ)、ピクセル 1 点は約 5 サイクル — 長さによりません。予算の性質が変わります:数えるのはピクセルではなく呼び出しです。

微妙な点は別の場所にあります。ファームウェアの線(EFLA アルゴリズム)は半開区間で、[P0, P1[ を描き終点を含みません。XOR では、辺ごとに描いた多角形は頂点を一つおきに失うか、二度点灯します。neo_gfx.s はこれを 2 つのプリミティブにしました:閉じた線(線 + 終点ピクセル)と、逆向きに描く「開いた」線 ]P0, P1]。こうして各頂点はちょうど一度だけ XOR されます。テストプログラム t_xor が冪等性を検証します:多角形を二度描くと画面は白紙に戻ります。

3. 座標:320 は 1 バイトに収まらない

Astéroric は 8 ビットの世界でした:240 列、すべてがレジスタに収まります。幅 320 ピクセルでは位置は int になり、8.8 固定小数点の速度積分は桁上げ付きで行います(phys.c)。トーラス状の衝突判定と複製によるラップアラウンド(端をまたぐオブジェクトは二度描く)は 320×240 で書き直し、スポーン・ラップ・HUD の定数は全画面のフィールドに合わせました。

4. タイミング:60 Hz のフレーム上で 30 Hz

VSync も Timer 1 もありません。ファームウェアは 60 Hz のフレームカウンタ(5,37)を公開しています。ゲームステップは 30 Hz — 2 フレーム — に固定し、25 Hz 用に較正していた定数はすべて計算し直しました。測定した遅延の下で、スプリント 1 は 255 ステップ中 3 ステップの遅れで間に合っていました。

その後プレイテストが、もっと丸い小惑星を求めました。gen_shapes.py で滑らかにした Atari のシルエットは 11〜13 頂点から 24/16/10 頂点へ。辺をつなぐ C のループは 1 辺あたり約 1,500 サイクルかかり、ゲームは 255 中 200 ステップの遅れに落ちました。多角形描画をアセンブラで書き直し(poly_xor:辺ごとのクリップ、半開区間の辺)、255 中 2 ステップの遅れに。このマシンのボトルネックはピクセルではなく、各呼び出しを包む C のオーバーヘッドです。

5. キーボードとサウンド

Oric の VIA/PSG マトリクスは、HID コードによる各キーの状態(1,2)に置き換わります。5 つのアクション — 回転、推進、ハイパースペース、発射、終了 — は CONTROLS 画面から任意の名前付きキーに再割り当てでき、マッピングは RAM に置かれます。サウンドは AY とその IRQ を離れ、ファームウェアの 4 チャンネル生成器(8,7)へ。Astéroric の各効果音はジングルも含め、矩形波/ノイズのノートキューとして作り直しました。ハードウェアのエンベロープがないため減衰は音量の階段です。9 月 16 日に neo で耳で確認 — エミュレータ上であり、ボード上ではありません。

6. ツールチェーンで注意した 2 点

cc65 2.19 と OptStackOps同じロジックをホストで gcc でコンパイルすると正しいのに、小惑星は線分のもつれとして表示されました。オプティマイザが C スタック上の演算を融合する際、頂点数のテーブルを形状 ID ではなくポインタの下位バイトで索引していたのです。回避策:プロジェクト全体で --disable-opt OptStackOps(呼び出しごとに数サイクル、6.25 MHz では影響なし)と、ポインタより先に n を読むこと。教訓:最適化パスを有効にしたら必ず基準キャプチャを通し直すこと。

きれいに終了する。NeoBASIC に戻るための jmp ($FFFC) はゲームを再起動していました。リセットベクタが .neo の実行アドレスにパッチされているからです。終了は今では API(1,3:NeoBASIC を再ロード)を経て jmp (0) — カーネルがリセット時に行うのと同じ。テスト t_exit は終了後に PRINT 6*7 を打ち、42 を期待します。

7. テスト

3 段階、すべて make test の中に。ホストでは、ファームウェアの EFLA を再現するスタブに対して移植可能なロジックを gcc でコンパイルします(8.8 積分、衝突、形状テーブル、XOR の冪等性、ラップアラウンド、分裂、キーテーブル)。ターゲットでは、Phosphoneo が決定論的なシナリオ — タイトル画面、1 ゲーム、CONTROLS、プログラム t_linet_xort_exit — を再生し、キャプチャを基準とビット単位で比較。テンポは共シミュレーションで記録した遅延の下で測ります。最後に公式エミュレータでのスモークテスト。この網が cc65 の事例、欠けた頂点、偽の NeoBASIC 復帰を捕まえました。

8. 私たちが主張しないこと

Neo6502 のボードはまだ一枚も接続していません。すべて Phosphoneo、本物のファームウェアとの共シミュレーション、neo で検証しています。ボード上の実際の呼び出し遅延は未測定(プラン B:ハードウェアスプライト)。サウンドはエミュレータ上で耳で確認しただけです。スプリント 2 は進行中:USB ゲームパッド、60 Hz オプション、SD/USB へのハイスコアとキーマッピングの保存。バイナリ asteroneo.neo(v0.2.0)は GitHub と Framagit で公開。リポジトリの CHANGELOG が正です。

出典とリンク

← Neo6502 · Prophet → · Neo6502 →