68000MPUのエミュレータって長大なものが多いけど、小ささ優先で書いたらどれくらいになるんだろう。

ということで、M68000PRM.pdfを見ながら書いてみました。
Z80エミュレータは書き始めたのが90年代だったこともあり、プリプロセッサマクロを多用しましたが、 今回はC++17で現代風に書きました。
# C++17をサポートしない環境では使えません。
少しでも速くするため、Z80のときと同様、使わないフラグは計算しない遅延評価を実装しています。

書きっぱなしでは動かないので、テストを探したところ、以下のものが見つかりました。

https://github.com/MicroCoreLabs/Projects/tree/master/MCL68/MC68000_Test_Code

問題があったらその直後で無限ループ、全て正常なら$f000で無限ループになる仕組みです。
これを以下のツールでアセンブルし、Sレコードを出力してテストを行いました。

http://www.easy68k.com

たくさんバグを潰しました。
で、これを使って何か動かしてみないと実感が湧きません。
68000を使っているマシンのエミュレータで、ソースが公開されていてMacでも動くものを探しました。
px68k(X68000)、Mini vMac(68kMac)あたりがよさそうです。
ソースを眺めてみましたが、Macはちょっと難しそうなのでpx68kにしました。
X68000は学生のころ憧れのパソコン、いやパーソナルワークステーションで、高くて買えなかったので今更ですが楽しんでみようと。
レトロパソコンの中で唯一(?)、BIOS(IPLROM)とOS(Human68k)が公式に無償公開されているのも素敵です。
ベースにしたのは64ビット環境に対応済みの下記のものです。

https://github.com/kenyahiro/px68k

まず、OSが立ち上がるまで毎回同じトレース結果にならないとデバッグが難しいです。
RTCが返す時刻が毎回同じじゃないとダメだろうというのは容易に予想できたので、そのように修正します。
そして、一つ命令を実行するたびにPCとメモリ(I/O含む)に書いた値を出力してみました。 再度実行すると、全く同じ結果になりました。
これならデバッグは楽です。
独自コアと切り替えられるようにし、ログを比べながらチクチクとデバッグしていきます。
このデバッグが楽しいんですね。楽しくないとやってられません。
10万命令まで同じになった、100万命令まで同じになった、画面左上にHuman68kと表示された、と。
そしてついにプロンプトまでたどり着きます。
# 余談ですが、N-BASICのプロンプトまでは約10万命令、Human68kのプロンプトまでは約1000万命令です。

Ko-Windowとかgccとか試してみて、動作が異なるたびにトレースしてデバッグしました。
ディスクに書き込む場合があるので、毎回ディスクイメージを復元することが必要でした。
KoMcpで再生マークが欠けることに気づいて元のコアに切り替えてもやっぱり欠けるということもありました。

最終的に、68000エミュレータのソースとヘッダ合計で1100行程度に収まりました。
68000って意外とシンプルなんです。

デバッグ中に自分の環境に合わせてpx68k自体の改変も行いました。

Mac用の変更
Mac用のXcodeプロジェクトを作成するとともに、ソースファイルをUTF-8にコンバートしました。
また、USキーボード配列に合わせました。
別途KeyWitch.xが必要です。添付のascii.envを使います。
ゲストのconfig.sysに
DEVICE = KeyWitch.x -e ascii.env
を追加します。

メモリの確保方法を変更
独自コアに載せ替えるにあたり、メモリをまとめて16MB確保するようにしました。
16MB確保できないプラットフォームでは動作しません。
また、SWITCH.Xで12MBに設定しても10MBまでしか使えなかったのを12MB使えるようにしました。

マウス操作を実装
Ko-Windowを試したいために実装しました。
ホストとゲストのマウスポインタを統合するため、添付のhostmouse.xを使います。
bgdrvとforkが必要です。
config.sysに
PROCESS = 16 2 10
を、autoexec.batに
bgdrv
fork -m200 hostmouse.x
を追加します。

メニューのトップレベルでEscキーで抜けられるように
もう一度F12を押せば抜けられるんですが、最初Escキーで抜けられなくて戸惑ったので。

ホストのVSyncを有効に
Time Profilerを見て、なんだか描画が重いと思って調べると、全体のループがホストのVSyncより速い周期で回っていました(No Wait ModeがOnのとき)。
そこで、SDL2のRendererを使ってホストのVSyncに同期するようにしました。
副作用としてSDL1.2は使えなくなりましたが、SDL2_gfxのインストールは不要になりました。

解像度1024x848まで対応
crtc.cを修正しました。
また、configのWinStretchが1のとき768x512固定でしたが、768x512より大きい場合はその大きさになるように変更しました。

負荷の軽減
No Wait ModeがOffのとき、ビジーループでゲストVSyncを待っていたので高負荷でした。
これをusleep()に変更しました(timer.c)。
また、Time Profilerで調べて少し重かったmfp.cを改良しました。

簡易ファイル転送機能
Windowsのeditdisk.exeというソフトでゲストのディスクイメージを書き換えていましたが、不便なので簡易ながらファイル転送機能を付けました。
file_transferディレクトリに入っています。
ホストからゲストに送る場合
ホストで
pxtrans < file
ゲストで
recv file
ゲストからホストに送る場合
ホストで
pxtrans > file
ゲストで
send file
とします。1つずつしか送れませんが、lzhファイルにまとめればなんとか使えます。

No Wait ModeとFrame Skipについて
No Wait ModeがOffの場合、Frame Skipは文字通りの意味です。
現在のマシンは十分速いので、フレームスキップする意味はなく、Full Frameに設定しておきます。
OnでFrame Skipが1/n Frameの場合、ホストVSyncの間隔でゲストVSyncの間隔のクロック数のn倍実行します。
ざっくりいえばn倍速になります。
もちろん、ホストCPU負荷が100%に達したらそれ以上速くなることはなく、フレームレートが低下します。
注) 音楽演奏などはNo Wait ModeがOffでないとタイミングが崩れ、飛び飛びになります。

M1 Mac MiniでKo-WindowのウィンドウサーバのCソース126本をコンパイルするのにかかる時間を調べました。
実機相当		5718秒
C68K最速		  39秒
独自コア最速	  58秒
独自コアはC68Kの7割程度の速さです。
なぜコンパクトな方が遅いのか。
C68Kを見てみると、フェッチしたオペコードから全パターン用意された処理ルーチンに直接ジャンプすることで、速度を稼いでいます。
goto *JumpTable[Opcode];
というコードを初めてみました。
処理ルーチンは数万行もある一つの関数になっており、ちょっと古いコンパイラでは最適化を有効にするといつまで待ってもビルドが終わりませんでした。
それに対し、独自コアは一つの命令を実行するのに何度か分岐をするので遅くなります。

ファイルはソースとヘッダの2つだけで、ヘッダのメモリアクセス部分をカスタマイズすれば組み込めるので、手軽に使うにはいいのではないでしょうか。

https://github.com/kwhr0/px68k

Xcodeから実行する場合、表層(Xcodeプロジェクトがあるディレクトリ)に*.xdf,*.hdfファイルを置くと
バイナリが生成されるディレクトリからリンクを張るようにしてあるので、F12メニューで選べます。

高速化

68000の複雑な命令をswitchでデコードするのはやっぱり重い。
ソースはなるべくコンパクトに保ったまま、もっと速くならないだろうか。
巨大switchをやめて16ビットのオペコードを配列の引数にしてダイレクトに呼び出す方法を試してみよう。
命令をグループごとに分けて、テンプレートを展開しまくって全パターン用意するようにすれば、(バイナリは大きくなっても)ソースはあまり大きくならないのでは?

ということで実装してみました。
結果、あまり速くならずに落胆したのですが、配列に入れるものをメンバ関数のポインタから普通の関数のポインタに変更することで結構速くなりました。
これは通常のcastでは変換できないので、unionを使って無理やり変換していますが、処理系依存なので動かない環境があるかもしれません。
x86_64/arm64、macOS/Linuxの4パターンでは大丈夫でした。
Windowsはダメかもしれません。

パフォーマンスをKo-Windowサーバのコンパイル時間で比較しました。
前回と同じ環境ではないため全体に時間がかかっていますが、今回の結果は以下のようになりました。
だいぶ速くなったのですが、それでもC68Kの「ジャンプでつなぐ」方式には及ばず、1割くらい遅いです。
全オペコードの配列は実行開始時に作成します。時間がかかりそうなので測ってみると、M1 Macで4mS程度でした。
ソースとヘッダの大きさの合計はそれほど増えず、1500行程度で済んでいるので、全体の見通しはいいと思います。
実機相当		7078秒
C68K最速		  45秒
独自コア最速	  51秒
従来の独自コア最速	  67秒