e-tipsmemo

ごった煮

NES Emulator in Rust : 1 Operation Cycle

e-tipsmemo.hatenablog.com

まずはどうCPU & PPU Cycleを合わせていくかというところを見る。
最初はほぼ参考サイトと同じように実装していく。

Operation Cycle

Generatorを使うことで、NESの1画面更新が、以下の1ループと同じになる。
yieldでVBlankで停止した時だけ、ループを抜け画面描写などをする

let mut nes_run = nes.run();
while true {
	loop {
		match Pin::new(&mut nes_run).resume(()) {
			GeneratorState::Yielded(NesStep::Ppu(ppu::PpuStep::Vblank)) => {
				// VBlank;
				break;
			}
			GeneratorState::Yielded(_) => {}
		}
	}

  //画面描写など
}


このNES Runを呼び出し側から 4回 resumeすることによって上から下まで実行することはCPUの1Cycleと同じ、かつPPUの3Cycleとなる。
必ずCPUとPPUのサイクルは1:3となり面倒なサイクル計算が必要なくなる。

pub fn run<'a>(&'a self) -> impl Generator<Yield = NesStep, Return = !> + 'a {
	let mut nes_cpu = Cpu::run(self);
	let mut nes_ppu = Ppu::run(self);

	move || loop {
		loop {
			match Pin::new(&mut nes_cpu).resume(()) {
				GeneratorState::Yielded(cpu_step @ cpu::CpuStep::Cycle) => {
					yield NesStep::Cpu(cpu_step);
					break;
				}
				GeneratorState::Yielded(operation) => {
					yield NesStep::Cpu(operation); //for nestest log
				}
			}
		}

		for _ in 0..3 {
			loop {
				match Pin::new(&mut nes_ppu).resume(()) {
					GeneratorState::Yielded(ppu_step @ ppu::PpuStep::Cycle) => {
						yield NesStep::Ppu(ppu_step);
						break;
					}
					GeneratorState::Yielded(ppu_step) => {
						//VBlank
						yield NesStep::Ppu(ppu_step);
					}
				}
			}
		}
	}
}

また、こういう実装方法をしていくと、borrow checkでいろいろとエラーでるのでCPUやPPUの構造体はCellで値を持つ。(マルチスレッドにはしないと思うので)

Addressing Mode & Cycle

どこのサイトにもあるが、対象とするレジスタとメモリのアドレッシングモードの組み合わせがあり、
アドレッシングモードによってCPU Cycle数が違う。
それらレジスタの種類 * アドレッシングモードの数 の組み合わせ全部を個別に実装するは大変となってくる。
これをTraitによって、実装を楽することができる。(参考サイトもそうなっている)

メモリ op レジスタ --> レジスタな計算に必要なアドレッシングモードと
BinaryOperation Traitを実装した計算(ORA、EOR、ADC、SUB、etc.)を引数にとる。

pub fn Imm_calc<'a>(
    nes: &'a Nes,
    op: impl BinaryOperation + 'a,
) -> impl Generator<Yield = CpuStep, Return = OpArg> + 'a {
    move || {
        Cpu::fetch_byte_inc_pc(&nes);
        yield CpuStep::Cycle;

        let value = Cpu::fetch_byte_inc_pc(&nes);
        op.calc_and_set(nes, value);
        yield CpuStep::Cycle;

        OpArg {
            addressing: Addressing::Implied,
            args: vec![value],
            value: AddressingResult::RWValue(value),
        }
    }
}

pub fn Xind_calc<'a>(
    nes: &'a Nes,
    op: impl BinaryOperation + 'a,
) -> impl Generator<Yield = CpuStep, Return = OpArg> + 'a {
   ////略
}
pub trait BinaryOperation {
    fn calc_and_set(&self, nes: &Nes, value: u8);
}

pub struct ORA;
impl BinaryOperation for ORA {
    #[inline(always)]
    fn calc_and_set(&self, nes: &Nes, value: u8) {
        let res = nes.cpu.a.get() | value;

        let mut p = nes.cpu.p.get();
        p.set(Status::Z, res == 0);
        p.set(Status::N, res & 0x80 != 0);

        nes.cpu.p.set(p);
        nes.cpu.a.set(res);
    }
}
////////////////////
/// ORA Operations
////////////////////
Opcode::OraXind => {
	yield_all! { Xind_calc(nes, ORA) }
}
Opcode::OraZpg => {
	yield_all! { Zpg_calc(nes, ORA) }
}
Opcode::OraImm => {
	yield_all! { Imm_calc(nes, ORA) }
}
.....

アドレッシングモードが同じでも、例えばSTAのIndirect.YとADCのIndirect.Yでサイクル数が違う気がしたので、
レジスタに対する操作別で分けたが不要だったかもしれない。
f:id:katakanan:20210815183519p:plain
opcodes and addressing modes – the 6502 – [ emudev ]

NES on FPGA CPUではIndirect,Yは全て 5 + 1サイクルで、同じだった。
(NESと6502で違うのか?)
また、もっとTraitを使いこなせばアドレッシングモードの実装をまとめることができる気がする。

その他

スプライトのbg透過を実装した。
ピーチは欠けたまま。
f:id:katakanan:20210815094940p:plain:w400