私のお気に入りの PowerMac は Nintendo Wii です
クラシック Mac OS を Wii と GameCube に移植する話。OpenBIOS と Trampoline から、偽の SCSI ドライバーでデスクトップに到達するまで。
日本語
コピー

(ゲスト投稿:Rairii)

今日、自分のプロジェクトのひとつがいい節目に到達した。ArtX チップセット(そう呼ぶのが気に入っている)を積んだ PowerPC 機(Nintendo GameCube、Wii、いずれは Wii U)にクラシック Mac OS を移植する作業が、初めてデスクトップまで到達したのだ。GameCube の Flipper 統合チップセットはまさに ArtX が設計したもので(ATI が同社を買収した時点で実質的に完成していた)、Wii の Vegas(および後期リビジョンの Bollywood)と Wii U の Latte は事実上その直系の後継にあたる。おそらく CHRP クローン時代以来、Apple の外の誰かがクラシック Mac OS を新しいシステム上でデスクトップまで起動したのは初めてだろう。公開リリースにはほど遠い状態だが、これまでこのプロジェクトでぶつかってきた課題について話しておきたい。
そもそも、なぜクラシック Mac OS をこのハードウェアに移植するのか。NT はもちろんすでに移植済みで、その作業の途中で PowerPC の Mac ノート PC を何台か手に入れ、そちらにも NT を移植することになった。片方の移植作業が他方にも効く、という具合だ。そうするうちにクラシック Mac OS に親しみ、いわゆる「jank」ぶりに惚れ込んだ。1980 年代まで遡るハックの上にハックを重ねたハックの数々は、間違いなく「jank」と呼べる。しかも TBXI の m68k 部分は「SuperMario」という名前で、PowerPC 時代を通じてずっとそうだった。1990 年代初頭の開発者がファンだったのだろうか。
あと、記録のために書いておくと、このプロジェクトを始めたのは 2025-06-22(upstream の openbios を fork した日)で、コーディングエージェントや LLM は一切使っていない。コーディング、リバースエンジニアリング、デバッグはすべて自分でやった。こういうプロジェクトは楽しみと勉強のためにやっているので、「AI」を使ったら楽しくないし、何も学べない。
まず最初のブートから。クラシック Mac OS の「New World」は「Trampoline」と呼ばれるブートローダーから始まり、これは Open Firmware 環境上で動く。つまり OpenBIOS を移植する必要があった。幸い、ちょうどこの頃に Goldfish64 が Wii への Mac OS X 移植を始めていて、彼が作った OpenBIOS の移植版からドライバをいくつか流用できた。ただ、いろいろあって彼の移植版をそのまま使うつもりはなかった(あれは Wii/Wii U しか対象にしていないが、自分は GameCube も対象にしたかった)。ドライバ開発を少し楽にするため既存コードの一部(具体的にはディスプレイドライバ)を再利用することにし、既存の NT ARC ファームウェアローダーをビッグエンディアン ELF をロードするように変更した。
起動時の試験に使うディスクドライバが少し必要だったので、EXI バス(SPI の Nintendo と Macronix における呼び名にすぎない)上のデバイス向けに書いた 2 つのドライバを移植することにした。「SD Gecko」(GameCube のメモリーカードスロットにそのまま挿すアダプタで、SPI 経由で SD を使えるようにするもの)と、「EXI-IDE」(実機に対しては正しくないドライバだが、自分の Dolphin ツリーにある不正確なエミュレーションに対しては正しく、エミュレータ上で低レベルデバッグ機能を使ったテストができる)だ。
ディスク上のフォーマットをどうするかは決めていなかった。以前の NT 移植版では、これらのドライバは FAT ファイルシステム上のディスクイメージだけで動いていた。FAT ファイルシステムから実行ファイルを読み込む既存の自作ソフト用ローダーとの互換性が求められたためだが、その方式では FAT32 の 4GB イメージという制約があり、後のバージョンの classic Mac OS はそれを超える容量を必要とした。そこで初期テストでは、「Mac OS ROM」の TBXI ファイルをそのまま FAT ファイルシステム上に\ppc\bootinfo.txtとして置き、起動時に OpenBIOS に読ませることにした。
これで Trampoline を動かせるようになった。パッチはできるだけ実行時に近い段階で、できるだけ多くイメージに当てる方針にした。そうすれば最終的には、手を加えていない既存の CD イメージをほぼそのまま使える。
最初にぶつかった問題は、Trampoline がこのシステムには当てはまらない物理アドレス空間のレイアウトを前提にしていたことだ。バッファの物理アドレスが不正なアドレスとして計算されてしまうので、必ず有効になるアドレスにパッチを当てた。カーネル用のメモリチャンクリストやページテーブルエントリを設定するコードにも、同じパッチを当てている。
Trampoline はさらに、0x8000_0000以降の物理アドレスがメモリマップド I/O に対応することを前提にしている(これまで動いていたシステムでは、このコードは PCI バスのアドレス空間に対応していた)。ここではそうではなく、Wii U ではこのアドレスは RAM の一部にあたる。壊れる前提はもう一つあった。物理アドレス空間上で RAM の領域の間に穴があり、その一部がメモリマップド I/O で埋まっているという前提だ。
カーネルのメモリチャンクリストにあるこの MMIO マッピングの初期化を、物理アドレス0x0100_0000(RAM のある領域の途中)から仮想アドレス0xE000_0000への長さ0x1000_0000のマッピングに差し替えて対処した。これで、MMIO 全体に加え、ある RAM 領域の末尾と(GameCube 以降の機種では)別の領域の先頭がマッピングされる。(将来変更するかもしれない。Wii U では物理アドレス範囲0xD000_0000-0xEFFF_FFFFが Radeon R7xx GPU の「HDP」と呼ばれる部分にマッピングされており、これを使えば特定のスウィズリングやエンディアン変換を伴う物理メモリのビューをマッピングできるため、GPU 固有フォーマットのテクスチャをリニアフレームバッファのように見せられる。このブロックを使う必要があるか次第だ。)Trampoline でマッピングを初期化する関数は途中で変わっており、結局は古いバージョンを検出してその関数をフックし、回避するようにした(古いバージョンは 16 ビットのページ数、新しいバージョンは 32 ビットを使う)。加えて、古いバージョンの Trampoline は0x8000_0000より上の物理メモリに対して無条件の 1:1 マッピングを設定するが、これは先に設定したマッピングと衝突するため削除する必要がある。
続いて、特定のハードウェアを前提とした各種パッチがある。すべての PowerPC Macintosh ハードウェアに搭載されているマイクロコントローラの存在に対応するビットフラグの設定を除去するもの、特定のハードウェアタイマーを使ってバス周波数を再計算しようとするもの、特定の割り込みコントローラデバイスに関する各種レジスタ書き込みとフィックスアップ(PCI バス上にあるためリトルエンディアンで、当然ながらここには存在しない)などだ。
最後に、ロード済みカーネルの初期化コードへジャンプする最終呼び出しをフックし、すべてがメモリにロードされた後に追加のパッチを当てられるようにしている。
加えて、デバイスツリーを Trampoline が受け入れる形に多少ハックする必要がある。割り込みコントローラは認識されるものの 1 つ(上述のリトルエンディアンのもの。レジスタレベルでは、すべての ArtX チップセットに存在する Flipper の割り込みコントローラに最も近い。もっとも Wii U では PowerPC コアの追加に対応するため多少変更されている)に見せかける。デバイスツリー内のフレームバッファのアドレスは非キャッシュの仮想アドレス(0xExxx_xxxxマッピング内)に変更し、最後に Mac99 互換の偽の電源管理デバイスをデバイスツリーに追加する。これにより Trampoline はそのための電源管理ドライバを用意し、後から差し替えられるようになる。
前述のとおり、カーネルの初期化にはフックを仕込んであるので、追加のパッチを当てられる。カーネル側で当てるパッチはひとつだけだ。カーネルには複数の割り込みコントローラドライバが含まれている。そのうち選ばれたものを Flipper の割り込みコントローラに合うようパッチし、使っていないドライバをひとつ犠牲にして空き容量を稼ぐ。ここでの主な違いは、期待される割り込みコントローラ(ドライバにパッチを当てる対象)がレガシーモードを持っていることだ。このモードでは、割り込みが発生したときだけでなく、割り込みの状態が変わるたびに PowerPC 割り込みを上げる。そのため、カーネル側で上がった割り込みをマスクし、あとからドライバの割り込みハンドラでアンマスクするという回避策が要る。
追加のパッチは「SuperMario」の m68k コードと、PowerPC のネイティブ実行ファイルにも当てる。m68k コードには、ネイティブの割り込みコントローラレジスタを直接読み書きするコードパスがいくつかあり、これらにパッチを当てる必要がある。オーディオ割り込みの有効化・無効化に使う関数ポインタはスタブに差し替える(そのテーブルにはスタブを指すエントリがすでにあり、助かる)。起動の早い段階で ADB と電源管理マイクロコントローラを初期化するために呼ばれる関数は、ハードウェアが実際に存在するかどうかを確認しない。存在しない場合、低位メモリの一部を破壊してしまう。ハードウェア割り込みを初期化する関数には、コピペのミスによるバグがある。シリアルポート 2 が存在しなければシリアルポート 2 の割り込み初期化コードを飛ばすつもりだったのだが、シリアルポート 1 からコードをコピペしたときにジャンプ先のラベルを変え忘れている。そのせいでシリアルポート 2 が存在しないと無限ループに陥る。ネイティブコードについては、割り込みコントローラのレジスタを読み書きするコードがほかにもあり、これにも正しくパッチを当てる必要がある。USBShimKeyboard ドライバの初期化コードは、USB コントローラが存在しない状態を扱えず、その場合は m68k エミュレータ内の NULL ポインタを呼び出してしまう。前述の電源管理ドライバもここでまるごと差し替える(現時点では BlueBox のスタブドライバを使っているが、シャットダウンと再起動をきちんとサポートするために、後で自前のものを書くつもりだ)。
TBXI がフロッピーの点滅画面まで起動するようになったところで、表示を正しく確認するためにディスプレイドライバを書いた(Elliott Nunn の virtio ドライバが大いに役立った)。続いて GameCube のコントローラポート用のドライバを書いて(同じ用途で書いた自分の NT ドライバからの移植だ)、USB なしでマウスカーソルを動かせるようにした。
ここで EXI デバイス用のブロック IO ドライバの実装に着手したが、ディスクフォーマットをどう設計するか、ドライバをどう動かすかで行き詰まってしまった。そんな折に CHRP 向け Mac OS が流出し、後の TBXI イメージにパッチを当てて起動させる作業に脱線した。7.x なら、8.0 の最低要件すら満たす RAM のない GameCube でも使えるだろうと考えたのだ。その後は別のプロジェクトに脱線した。それからほぼ 1 年後、この作業に戻って OpenBIOS に好みのディスクフォーマットを実装し、SCSI を偽装するドライバをでっち上げることにした(同じ手法を取ったドライバは他にもあり、特に各種 SATA コントローラ向けのクラシック Mac OS ドライバが有名だ)。この偽 SCSI ドライバを書く中で、いくつか問題も見つかった。m68k のコードにさらにパッチを当て、ディスクドライバ(ディスクそのものから読み込まれるドライバで、要するにパーティション切られたブロックデバイスの上に実際のディスクデバイスを実装するもの)を旧タイプではなく新タイプとして強制的に読み込ませる必要があった。旧タイプは最初の PowerPC 機が登場した時点で事実上廃れていたし、そもそも自分の偽 SCSI ドライバは旧式の呼び出しに対応していない。旧タイプのドライバとして読み込もうとしてクラッシュしたのはおそらくそのせいだ。さらに面白い癖も見つかった。光ディスクドライバ(これもブロックデバイスから読み込まれる)は、2048 バイトや 2352 バイトのセクタを持つブロックデバイス上で 512 バイトセクタをエミュレートする際、scatter-gather バッファを 3 つ用意する読み込み呼び出しを行う。ところが、下層の SCSI ドライバが scatter-gather に対応しているかを一切確認していなかった。そのため、読み込み側で scatter-gather を実装する必要があった。
フロッピーの点滅アイコンはまだ出ていたが、デバッグを進めるうちに何が弾かれているのかが見えてきた。起動デバイスを取得するために PowerPC の関数StartLib!GetStartupDeviceが呼ばれており、この関数はデバイスツリーと、個別の扱いを必要とする組み込み SCSI デバイスの存在を前提にしていた。前者は簡単で、ドライバの初期化コード内でデバイスツリーを修正すれば済んだ。後者の方が面白く、内部構造を掘り起こして 3 か所(デバイスツリーを含む)のインデックスを非ゼロに修正し、正しいコードパスが実行されるようにする必要があった。
ここまでくると、初めてマックの笑顔アイコンが表示された。だが次は HFS のブートセクタで弾かれた。何かがエラーになって戻ってきたのだ。原因は自分のドライバのバグだった。Dolphin 上で EXI-IDE デバイスをでっち上げてデバッグしていたが、IDE デバイスの PIO モードでの読み書きは 16 ビットワード単位で行われる。まさか完全にアラインメントの外れた奇数アドレスのバッファ(下位 4 ビットが 1)が渡されるとは思っておらず、そうしたアドレスでは EXI DMA をそもそも試みないよう低レベルドライバを修正した。このバグは、実質的に 1 バイトが書き出されず、残りのデータが 1 バイトずれてしまう形で現れた。せめてもの救いは、これがリソースフォークのヘッダを読むときに起きたことと、失敗したのがそのデータの妥当性チェックだったことだ。おかげでデバッグはずっと楽で、最終的には RAM をダンプし、使っていたディスクイメージ上の期待値と比較することになった。
その後、改造した Dolphin ツリー上で Mac OS 9.2.2 が起動し始めた。ただし非常に遅く、1 時間かけてようやくクラッシュした。次に 7.6 を試すと、こちらもクラッシュしたが、より早い段階だった。おそらく MacsBug を読み込もうとして落ちているのだと気づいて削除すると、約 20 分後にデスクトップが目の前に現れた。

翌朝、実機でテストした(SPI 接続の SD を使用)。7.6 が 40 秒以内で起動した。SD カードのアクセスモードとしては最も遅いはずの方式だ。もっとも、以前に SPI 接続 SD 用の NT ドライバを書いた経験があり、そちらのほうがむしろ例外かもしれない。このブロックデバイスを使って GameCube に NT をインストールしているこの動画を見てほしい。NT のテキストセットアップのファイルコピーが 64 倍に高速化されている点に注目。とにかく遅いのだ。ここでの違いは、転送中に CPU を丸ごと占有できることにある。実際、他に選択肢はない。転送が非同期であることを示す値を返すと、呼び出し側は転送完了を待って割り込みをループでビジーウェイトする(ただしこのビジーウェイトはシステム上のすべてのコントローラを確認している)。
その後、MacsBug で何が起きているかをデバッグできるよう、間に合わせのクラッシュハンドラを追加した。原因はエミュレーションのエラーで、実機で MacsBug をロードしたときにはクラッシュしなかった。次に分かったのは、Mac OS 9.2.2 が起動時にクラッシュするのは Open Transport のせいだということだった。Extensions フォルダから外すと問題なく起動した。シリアルデバイスもネットワークデバイスも存在しない状況を Open Transport が扱えないのだろう。
前述のとおり、私の移植版はまだリリースには程遠い。次は Wii の SD コントローラ、続いて USB コントローラ(これについてはすでにいくつかリバースエンジニアリングを進めている。vibecoded の EHCI ドライバが存在するのは知っているが、まあリバースエンジニアリングの助けになる程度だろう)、その後で Wii U 上での移植版の立ち上げに取り組む。いずれは RAM の壁を少なくともいくつか突破するためのパッチも試してみたい。