まずはどう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でサイクル数が違う気がしたので、
レジスタに対する操作別で分けたが不要だったかもしれない。
opcodes and addressing modes – the 6502 – [ emudev ]
NES on FPGA CPUではIndirect,Yは全て 5 + 1サイクルで、同じだった。
(NESと6502で違うのか?)
また、もっとTraitを使いこなせばアドレッシングモードの実装をまとめることができる気がする。