17
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ファミコンのエミュレータをRustで書いた

Posted at

男もすなるNESエミュ作成といふものを、女もしてみむとてするなり。

概要

いろいろな人がやっているエミュレータ作成を、何番煎じかわからないけどやってみました。
ひとまずそこそこ動いたので公開し、ハマったポイントなどを紹介します。

成果物

githubリポジトリ

参考にすべきサイト

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を利用した方がよいと思われます。

おわりに

とりあえず書いて見ましたが、ここの解説が欲しい、ここが間違っているなどがありましたら、気軽にコメントください。

17
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?