2004年度黄金週間突発企画 1週間でm5エミュレータを作ろう日記 2004/4/29 EmuZ-5500/6500が遅々として進まないので、ゴールデンウィークの代替企画として m5エミュレータを作ってみようと思い立つ。 moriyaさんのサイトとMESSのコードを見る限りでは、規模は小さそうなので、 ゴールデンウィーク中の突発ネタとしては適当だろう。 単なる車輪の再発明になってしまうが、単体エミュレータとしての需要はある だろうし、以前ぶちあげたエミュレータスケルトンの教材として使うという 目的もあるので、まあよし。 技術資料はmoriyaさんのサイトおよびMESSのコードから。 (現在帰省中で、手元に実機がないので) 目標は、連休(今年は1週間)中に完成させること。 まずは基本設計。 エミュレーション管理クラスとしてEMUクラスを定義。 入力、ウィンドウ描画、音源再生などwin32依存な部分は全部この中で行う。 EMUクラス内で仮想マシンクラスVMを生成。 VMクラスには親であるEMUクラスのポインタを渡しておいて、VM内でwin32側を 操作したい場合(例えば、キーボードの状態を取得したい、など)は、この ポインタを経由して、EMUクラス側のwin32処理部分にアクセスする。 winmain側からはEMUクラスのみを操作。 emu->run();で1フレーム分だけエミュレーションを行う。 タイミング調整は、ウィンドウメッセージの処理の兼ね合いもあるので、 敢えてwinmain()側で行う。 リセットやカートリッジ挿入などについても、EMUクラスを経由してVMクラスを 間接的に操作する。 最初はEMUクラスの実装から。 win32依存部分は、EmuZ-2500で作成したコードを切った貼ったで作成。 入力関係。 WM_KEYDOWN, WM_KEYUPを受け付ける関数を用意して、キーの状態を取得する。 但し、caps/kana/kanjiだけは、キー操作とメッセージの来るタイミングが 変なので、フレームごとに調整を行う。 ジョイスティック入力は、フレームごとに取得して更新。 今回は使用しないが、マウス入力についても取得する仕組みを用意する。 画面操作周りは通常のGDIとして実装する。 画面サイズにあわせて15bppのDIBSectionを生成。 後はwinmainからの要求に従ってbitblt/stretchbltするだけ。 またVM側にDIBSectionへのポインタを渡す仕組みも用意する。 音源周りはDirectSoundを使用する。 バッファを作成して、無限ループで再生。 フレームごとに再生位置を取得して、バッファの前半・後半が切り替わった タイミングで、VM側から生成した波形を受け取ってコピーする。 後はVMクラスへのインターフェース(run, reset, insert_cartridgeなど)の 雛形だけ適当にでっちあげ。 VMクラス側。 まずは各デバイスの基底クラスとしてDEVICEクラスを設計する。 CPU以外の各デバイスは、DEVICEクラスの派生クラスとして実装する。 DEVICEクラスは、以下の仮想関数を持つ。 virtual void initialize() {} virtual void release() {} virtual void reset() {} virtual void write_data(uint32 addr, uint8 data) {} virtual uint8 read_data(uint32 addr) { return 0xff; } virtual void write_io(uint32 addr, uint8 data) {} virtual uint8 read_io(uint32 addr) { return 0xff; } virtual void write_signal(int ch, uint32 data) {} virtual void event_callback(int event_id, int clock) {} 今回は関係ないが、例えばDMAを実装する場合は、DEVICEクラスのポインタとして FDCクラスとメモリクラスを渡してやって、DMA側ではwrite_data, read_dataを 経由してデータのコピーを行う。 また、VMクラスにDEVICEクラスのポインタとしてデバイスのポインタを渡して、 一定周期ごと、または指定した期間後にevent_callback関数がコールされる ようにすることで、例えばZ80CTCのタイマーモードを実装したり、FDCでシーク コマンドが発行されたら、一定期間ごとにヘッダを動かす、といった処理を 実装することができる。 write_signalは、各デバイスの入力信号用。 例えば、m5では、VDPの割り込み信号がZ-80CTCのCLK3信号に接続されており、 Z-80CTC経由で割り込みを実行している。 これを実装するにあたっては、VDP内で、 vm->ctc->write_signal(3, 1); vm->ctc->write_signal(3, 0); とすることで、Z-80CTCのCLK3に、H->Lという信号を送ることができる。 後は、VMクラスの骨格部分を実装。 VM::VM()内で、各デバイスのクラスを生成。 VM::~VM()内で、開放。 データバスの実装。 CPUからメモリやI/Oを操作するときは、vm->read_memory()などとする。 CPUを1フレームだけ駆動する処理。 CPUを1ライン分だけ駆動、タイマーコールバックを1ライン分だけ駆動、 という処理をライン数だけ繰り返す。 またVBlankのon/offや、仮想画面の生成の指示もここで行う。 winmain側の実装。 ウィンドウを表示して、EMUクラスを生成。 メッセージループ内で、タイミングを調整しつつ、エミュレーションコアを駆動。 時間に余裕があれば、ウィンドウの更新を行う。 後は、ウィンドウメッセージに従って、EMUクラスにキー入力を送ったり、 WM_PAINTに対して画面更新をEMUクラスに要求する処理など。 ここまで実装して、コンパイルが通ることを確認したところで、今日は作業終わり。 2004/4/30 VMクラスの骨格部の実装の続き。 各デバイスからのIRQの調停と、CPUへの橋渡し。 CPU内で、RETIやDIが実行されたときの処理などを行う。 これについては、基本的にはEmuZ-2500の実装を流用する。 各デバイスへのタイマーコールバックの実装。 run()関数内でCPUを駆動するのにあわせて、こちらのカウントも進める。 規定のカウントに達したらイベントコールバック発生、あとはループイベントか 否かに応じて作業を行う。 試しに、Z-80 CTCの一部を実装して、タイマーが駆動されていることを確認する。 CPUの実装。 これについては、DEVICEクラスの派生クラスとしては実装しない。 基本的には、EmuZ-2500やCP/M Playerで使用しているものを流用する。 メモリの実装。 moriyaさんの解析結果を参考に、メモリマップは以下の通りとする。 0x0000 - 0x1fff : Internal ROM (8KB) 0x2000 - 0x6fff : External ROM (8KB or 16KB or 20KB) 0x7000 - 0x7fff : Internal RAM (4KB) 0x8000 - 0xffff : External RAM (32KB) 0x6000 - 0x6fffはreservedとなっていたが、BASIC-Fが20KBだけあることから、 ここに末尾4KB分がマップされるものと考えられる。 バンクは0x1000単位として、uint8*型のポインタでバンクの指すメモリ領域を 指定する。 読み込みと書き込みでバンクを別にして、rom領域は各々異なる領域を指すことで、 romの内容が破壊されないようにするのは、EmuZ-2500と同様。 キーボード&ジョイスティックの実装。 これは単純に、EMUクラスから入力デバイスのステータスを受け取って、 マトリックスに応じてデータを生成するだけ。 トリガボタンは、#1, #2各々について、キーボードの1, 2, 5, 6に当たるのかな? Z-80CTCの実装。 タイマーモード時は、タイマーコールバックをループで登録して、これによって カウンタを進める。 外部からのクロック入力については、write_signal関数内で実装。 TMS9918Aの実装。 取り敢えず画面生成部は後回しにして、レジスタ及びVRAMの操作周りを実装する。 またvblankにあわせて、Z-80CTCのCLK3に、H->Lの信号を送るようにする。 ここまで実装したところで、動作確認。 I/Oのログを見る限り、なんかプーヤンが動いている、ような気がする、らしい。 画面も音も出ないから、さっぱり判らないが(苦笑) このままだとどうにも気持ち悪いので、音源部の実装を進める。 SN76498ANでは、各チャンネルごとにボリュームとカウント数を指定。 で、外部クロックを指定したカウント数だけ入力したらon/offを切り替えることで 矩形波を生成、後はボリューム分だけ合成すればいいだけっぽい。多分。 タイマコールバックで10msecごとに波形を生成して、win32側から要求されたら (まだ規定サンプル数だけ波形を生成していなかったら全部満たした上で) 生成したサウンドバッファを渡してやる。 win32側は1フレームごとに再生位置を確認、バッファ前半・後半が切り替わったら vm側から生成した波形を受け取って、DirectSoundのバッファにコピーするだけ。 で、実装してみたところ、無事プーヤンのBGMが再生されていることを確認。 めでたい。 2004/5/1 TMS9918A描画処理実装。 基本的にはMESSの処理を参考に、win32側とのインターフェースのみ独自実装。 先にパレット番号で仮想画面を生成して、そのあとで、15bppでDIBSectionの バッファにコピーをするようにする。 ここまで実装したところで動作確認。 プーヤンについては、正常に動作しているらしいことを確認する。 まんせー、うらー、じーくかいざー、びばでもくらしー。 既存部品を流用して組み合わせた部分が多いとは言え、3日で一応動くところまで 来たのは、正直予想以上でした。 如何にm5がシンプルな構成をしているか、よく判りますね。 プリンタ周りやカセットレコーダについては、また後日に。 HPC2000やPPC2002対応を優先的に行う予定です。 ROMデータの吸出し法などについては、連休が終わって、会社の独身寮に戻ってから あらためて纏めます。 公開後に、moriyaさんから色々ご指摘頂いたところを修正。多謝多謝。 EVENT_MAXの宣言が衝突していたので、CALLBACK_MAXに変更。 DIBSectionの開放のところでメモリ破壊が起きていた問題を修正。 あぶねーあぶねー。 フレームレートが50Hzだったのを、日本仕様にあわせて60Hzに変更。 で、タイミング調整の処理部において、1フレームの間隔 1000/FRAMES_PER_SEC の 整数化による誤差が蓄積してしまうという問題が顕在化。 (従来は50Hzのため、たまたま顕在化しなかった) int interval[FRAMES_PER_SEC], interval_cnt = 0, remain = 1000; for(int i = 0; i < FRAMES_PER_SEC; i++) { interval[i] = (int)(1000 / FRAMES_PER_SEC); remain -= interval[i]; } for(int i = 0; i < remain; i++) interval[(int)(FRAMES_PER_SEC * i / remain)]++; このようにして、1秒間内の各フレームごとのインターバルを、合計で1000msecに なるように、大体均等に振り分けてやることで対応する。 2004/5/2 家族で龍神温泉まで旅行。 明日は高野山詣での予定。 開発機材は持ち込んでいるので、軽く作業を行う。 まずはHPC 2000への対応。 eVC++3.0で適当にプロジェクトを作成してビルドしてみたところ、dsound.hが 見つからないと怒られる。 …あー、DirectSound対応ってeVC++4.0からでしたっけ? 取り敢えずサウンド周りを#ifndefで括って対処。 後はメニューバー周りの実装と、ウィンドウ生成周りを修正。 まあ、この辺りはお約束だし、最近もCP/M Playerでやったばかりなので、 特に迷うようなところはなし。 出来上がったバイナリをsigmarion3に持っていって、xclkcfgで533MHzにして 実行したところ、大体30fpsは出ている模様。 画面サイズを256x192にしているので、一番パワーを食うBitBlt()も、然程は 影響していないのかな。 あっさり動かれると、それはそれでつまらない気もします。 訳のわからないところで、意味もなく落ちられるのもきついですが。 開発環境はeVC++3.0のままにしたいので、サウンド周りは何とかwaveOut系APIで 実装することを考える。 この辺りは、スケルトンということで選択の余地を増やしておきたいですし。 実装に関しては、基本的にはEmuZ-2500のwaveOut系処理そのまま。 (更にオリジナルはM88。多謝>ciscさん) 但し、再生完了通知はスレッド受け取りからウィンドウ受け取りに変更。 1/60secだけ音が途切れる可能性があるが、どうもCEだとマルチスレッドは いまいちパフォーマンスが出ないらしいので。 8KHzにすれば、一応そこそこ聞けるレベルになる模様。 フレームレートも24fpsと、まあ許容範囲か。 2004/5/3 高野山は広かった。 帰ってきてから反応を見ると、一部筋で静かな反響を呼んでいる模様、うむうむ。 取り敢えず、sigmarion3で画面中心に表示できるようにする。 適当に画面サイズを取得して、BitBltのdestの値を弄るだけ。 PPC2002については、デバイスが手元にないので、独身寮に帰ってから。 2004/5/5 会社の独身寮へ帰還。明日から仕事の日々、だるー。 取り急ぎ、PPC2002の対応を行う。 この辺りはやりなれた作業なので、特に戸惑うこともなし。 WM_CHARで受け取ったコードをキーストロークに変更してemuクラスに渡すのと、 フルスクリーンやSIPの制御周りだけ弄ってやればよし。 Windows98SEで落ちるという件は、また後日。申し訳ない。 2004/8/25 長らく放置してしまって済みませんです。 Win9x系で落ちる原因は、DIBSection作るときにbiCompressionにBI_BITFIELDSを 指定してたことでした。 あと、1氏の楽がきスレでのご提案に応じて、擬似フルスクリーンをサポート しています。 205/5/23-24 EmuGakiの更新に引き続き、こちらも更新。 Emu5からEmuGakiを作成して、今度はEmuGakiからEmu5を作成、ですか。 データレコーダのサポートのための作業もしているのですが、セーブは出来ても ロードが上手くいかず。 205/5/25 ネット上を検索していて、m5のロードコマンドはOLDだということを知りました。 …動くやん(爆発炎上