男もすなるNESエミュ作成といふものを、女もしてみむとてするなり。
概要
いろいろな人がやっているエミュレータ作成を、何番煎じかわからないけどやってみました。
ひとまずそこそこ動いたので公開し、ハマったポイントなどを紹介します。
成果物
参考にすべきサイト
Nesdev
ここを見ましょう。ほぼ全て載っています。テストROMもあります。
NES on FPGA
書き始めはこのサイトにおせわになりました。
実装にあたり検討すべき事項
CPU命令の実装粒度
1クロック毎の動作を実装するか、命令毎に実装する(数クロック分を1つの処理として実装)か検討する必要があります。
よくある日本語の記事では、命令毎に実装していますが、厳密なエミュレーションはできないと思われます。
厳密に実装するのであれば、ダミーの読み書きや、IRQのタイミングなども制御できるメリットがありますが、
実行速度が犠牲になるので、要検討事項だと思われます。
今回は次のような実装としました。
pub(crate) enum CpuStepStateEnum {
Continue,
Exit(CpuStatesEnum),
}
pub(crate) trait CpuStepState {
fn exec(
core: &mut Core,
ppu: &mut Ppu,
cartridge: &mut Cartridge,
controller: &mut Controller,
apu: &mut Apu,
) -> CpuStepStateEnum;
}
pub(crate) enum CpuStatesEnum {
FetchOpCode,
Reset,
Irq,
AbsoluteIndirect,
AbsoluteXRMW,
AbsoluteX,
// 以下同様にアドレッシングモード関連が続く
And,
Eor,
Ora,
// 以下同様に命令が続く
}
次のように利用します
while let CpuStepStateEnum::Exit(s) = machine(self, ppu, cartridge, controller, apu) {
// 次の実装を呼び出す
}
書いて見たところ、あまり綺麗な実装とは言い難いので、要検討事項でしょう。
DMA転送とAPU DMCと例外の取り扱い
競合します。どう実装するか考えておいた方がよいかと思います。
今回の実装では後から追加したため、実装上の課題事項として残ってしまってます。
例外のタイミング
厳密に作ろうとするとかなり厄介です。NesDevを見たりしながら実装し、テストケースは通過しておりますが、
いまだによくわかっていません。
オープンバスの取り扱い
オープンバスに書き込んだ内容は時間経過と共に減衰します。
今回は次のような実装にしました。
struct DecayableOpenBus {
data: u8,
decay: [u8; 8],
}
impl DecayableOpenBus {
pub fn new() -> Self {
Self {
data: 0,
decay: [0; 8],
}
}
pub fn unite(&mut self, data: OpenBusReadResult) -> u8 {
for i in 0..8 {
if (data.mask >> i) == 1 {
self.decay[i] = 20;
}
}
let result = (self.data & !data.mask) | (data.data & data.mask);
self.data = result;
result
}
pub fn write(&mut self, data: u8) -> u8 {
self.data = data;
self.decay = [20; 8];
data
}
pub fn next(&mut self) {
let mut result_mask: u8 = 0;
for i in 0..8 {
if self.decay[i] > 0 {
self.decay[i] -= 1;
result_mask |= 1 << i;
}
}
self.data &= result_mask;
}
}
実行速度の基準
60FPSを基準に実行速度を考えた場合、サウンドにズレが発生します。
サウンドの再生速度については、ハードウエア側で制御されることから、
そちらを基準にしてもよいかもしれません。
NTSCとPALの違い
対応するか事前に検討しておく方が良いかと思います。
特に1CPUクロック=3.2PPUクロックとなってしまうことから、
どちらかというとPALの方が実装が厄介そうです。
今回は対応しておりませんが。
ステートマシン
CPUはステートマシンであることから、実装においても念頭に置く必要があると思われます。ステートマシンの種類にはミーリマシンとムーアマシンがありますので理解しておいたよいかと思います。
GTK+
macにおけるOpenGL WidgetであるGLAreaにはv3.24現在、リサイズが正しく動作しない不具合があるようです。
mac自体がOpenGLを非推奨としていることもあり、どうしたものか検討中です。
OpenAL
実装をmacで実施したことから、CoreAudioをOpenALで叩いております。
OpenALはかなりお手軽なのでこのまま行きたいのですが、
Androidだと遅延が激しいと聞いています。
今後Android等で実装する場合はせめてOpenSL ESを利用した方がよいと思われます。
おわりに
とりあえず書いて見ましたが、ここの解説が欲しい、ここが間違っているなどがありましたら、気軽にコメントください。