CPU
#[derive(Default, Clone)] pub struct Cpu { pub pc: Cell<u16>, pub a: Cell<u8>, pub x: Cell<u8>, pub y: Cell<u8>, pub s: Cell<u8>, pub p: Cell<Status>, pub nmi: Cell<bool>, }
レジスタを用意する。Statusはbitflags crateでflag管理する。
メインメモリはNESが持っているので、メモリアクセスはNESのメンバ関数にしてある。
ここら辺は参考サイトとほぼ同様
Fetch / Decode / Exec
//Not consume Cpu Cycle let pc = nes.cpu.pc.get(); let bin = nes.read8(pc); let opcode = num_traits::FromPrimitive::from_u8(bin) .unwrap_or_else(|| panic!("Unknown Opcode 0x{:02X} @0x{:04X}", bin, pc)); let oparg = match opcode { //////////////////// /// LDA Operations //////////////////// Opcode::LdaXind => { yield_all! { Xind_load(nes, RegA) } } Opcode::LdaZpg => { yield_all! { Zpg_load(nes, RegA) } } .... //////////////////// /// LDY Operations //////////////////// Opcode::LdyImm => { yield_all! { Imm_load(nes, RegY) } } Opcode::LdyZpg => { yield_all! { Zpg_load(nes, RegY) } } ....
公式命令の動作の説明は、いろいろなサイトで解説されており、基本的な動作をするものばかりであった。
アドレッシングモードのタイミングさえ合わせれば難しいことはない。
いくつかのアドレッシングモードでのアドレス計算ではオーバーフローを無視したりする計算があり、注意。
フラグを更新するときに使いまくっているstd::cell::Cellのupdateメソッドは
feature cell_updateが必要。
#[inline] #[unstable(feature = "cell_update", issue = "50186")] pub fn update<F>(&self, f: F) -> T where F: FnOnce(T) -> T, { let old = self.get(); let new = f(old); self.set(new); new }
実装抜けがないようにチェック(nestestドリブンで実装したほうがよかった気がする)
nestest.nes
命令実装で役に立つのが、nestest.nes。
if cfg!(feature = "nestestlog") { nes = nes::Nes::start("../nes-roms/nestest.nes"); nes.cpu.pc.set(0xC000); nes.cpu.p.set(cpu::Status::from_bits_truncate(0x24)); } else { nes = nes::Nes::start("../nes-roms/sample1.nes"); }
0xC000にジャンプし、フラグを0x24、スタックアドレスは0xFD(デフォルト)とするとPPUなしでCPUの動作テストができる。
その動作結果を既存のlogと比較することで、ミスを発見し、直す。
https://www.qmtpro.com/%7Enes/misc/nestest.log
このlogですこし面倒だったのは、
左側にはあるアドレスで実行されるに命令とオペランドの組み合わせがかかれているが、
右側にあるのはその命令を実行する前のCPUの状態とPPU、CPUサイクルということだった。
C8D1 B8 CLV A:00 X:00 Y:00 P:6F SP:FB PPU: 4, 19 CYC:461 C8D2 A9 F8 LDA #$F8 A:00 X:00 Y:00 P:2F SP:FB PPU: 4, 25 CYC:463 C8D4 29 EF AND #$EF A:F8 X:00 Y:00 P:AD SP:FB PPU: 4, 31 CYC:465
2行目のLDX #$FBが実行された結果、3行目の右側の状態となる。
このエミュレータがGeneratorを使用しているので、
左側の命令の情報がすべて得られている時にはすでに、fetch/decode/execが実行された後となり、(とくにCPUサイクルは進んでいる)
エミュレータ内部で持っているCPUの状態はすでに変わっているので
前の状態をわざわざ保存しておかなければならなかった。
loop { match Pin::new(&mut nes_cpu).resume(()) { GeneratorState::Yielded(cpu_step @ cpu::CpuStep::Cycle) => { cpu_cycle = cpu_cycle + 1; yield NesStep::Cpu(cpu_step); break; } GeneratorState::Yielded(operation) => { if cfg!(feature = "nestestlog") { println!( "{:<48}{} PPU:{:>3},{:>3} CYC:{}", cpu::debug_op(&operation), cpu::debug_reg(®s_before_exec), ppu_line_before_exec, ppu_cycle_before_exec, cpu_cycle_before_exec ); cpu_cycle_before_exec = cpu_cycle; ppu_cycle_before_exec = ppu_cycle; ppu_line_before_exec = ppu_line; regs_before_exec = self.cpu.clone(); } yield NesStep::Cpu(operation); } } }
汚い。
nestestを通しつつある。(WinMergeUを使用)
0xC6BCまでが公開されている命令セットのテストとなる。ここまで通っていれば大体のゲームは動くらしい。
Undocumented Instruction
せっかくなのでnestest.logを通すためだけのUndocumented Instructionも実装した。
いくつかのUndocumented ISAは、既存のOfficial ISAが組み合わさったような動作を行う。
https://www.nesdev.com/undocumented_opcodes.txt
例えば、
ISC (ISB) [INS] =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D Increase memory by one, then subtract memory from accu-mulator (with borrow). Status flags: N,V,Z,C
なので INCとSUBが組み合わさっている。
pub struct ISB; impl MemCalcOperation for ISB { #[inline(always)] fn calc(&self, nes: &Nes, value: u8) -> u8 { let tmp = (value as u16 + 1) as u8; //Increase memory by one let c = nes.cpu.p.get().contains(Status::C) as u16; let a = nes.cpu.a.get(); let not_value = !tmp; let res = (a as u16) + (not_value as u16) + c; //then subtract memory from accu-mulator //A - b- !c = A + (not b) + c let mut p = nes.cpu.p.get(); p.set(Status::Z, res & 0xFF == 0); p.set(Status::C, res & 0x100 != 0); p.set(Status::N, res & 0x80 != 0); p.set( Status::V, (a ^ res as u8) & (not_value ^ res as u8) & 0x80 != 0, ); nes.cpu.p.set(p); nes.cpu.a.set(res as u8); tmp } }
アドレス0xC68B以降は入力やAPUなのでとりあえず無視。0xFFを返すようにしておけば問題ない。
この記事を書きながら、nestestにないUndocumented ISAもあることに気づいたが。
それらはやる気が出たら実装する。
HLTとかASR (ASR) [ALR]....
こうして命令実装はかなり順調に進む。
SUBのオーバーフローなどはわかりにくいが検索するとすぐ出てくる。
Determining carry and overflow flag in 6502 emulation in Java? - Stack Overflow
大半のアドレッシングモードを計算してから、オーバーフローを無視するアドレス計算では、wrapping_addといった関数を使えば、かなり楽できたということを知った。
(知らなかったのでがんばって符号拡張したりしている。しかし符号拡張が間違っているところでは実行中にoverflow panicを起こしたりするかもしれない。)
こういう基礎的なところを知るためにも、NESエミュレータ作成はプログラミング言語入門には最適だと改めて感じた。
またnestestを通せば最低限CPUは間違っていないという安心感があるので、心おきなくPPU実装に移ることができる。
実際、background機能だけのPPUを実装後、nestsetを実行すると一発で通った。
PPUはバグっているが、nestestは通っていることがわかる。