added public API interface and step shell checks for following implementations

This commit is contained in:
2026-08-09 15:26:35 -05:00
parent 232108c522
commit dc38ff8aa4
2 changed files with 51 additions and 12 deletions
+24 -12
View File
@@ -36,24 +36,32 @@
- Batch 7 done (logic): AND (8 modes), ORA (8 modes), EOR (8 modes) via three cores (and/ora/eor) + wrappers + immediate variants. Only Z/N flags affected, C/V untouched. Caught a real bug in review: all three _izx wrappers used base 5 instead of 6. 112 of 151 official opcodes done. Huge diff progress: carried the log from 73 to 506 lines - Batch 7 done (logic): AND (8 modes), ORA (8 modes), EOR (8 modes) via three cores (and/ora/eor) + wrappers + immediate variants. Only Z/N flags affected, C/V untouched. Caught a real bug in review: all three _izx wrappers used base 5 instead of 6. 112 of 151 official opcodes done. Huge diff progress: carried the log from 73 to 506 lines
- Match compaction done: step and opcode_len both reformatted to max 4 opcodes per line. step match grouped by family with header comments; opcode_len keeps per-entry /* */ mnemonics. Diff verified unchanged after compaction - Match compaction done: step and opcode_len both reformatted to max 4 opcodes per line. step match grouped by family with header comments; opcode_len keeps per-entry /* */ mnemonics. Diff verified unchanged after compaction
- trace bytes are length-aware via opcode_len (fixed-width byte field, columns align for awk diff) - trace bytes are length-aware via opcode_len (fixed-width byte field, columns align for awk diff)
- MMC1 Phase 1 done (src/mapper.rs): Mirroring enum (5 variants incl. OneScreenLow/High), trait Mapper: Debug, Nrom impl, Mmc1 impl (shift-register protocol, PRG 32K/16K banking, CHR 8K/two-4K banking, init control=0x0C)
- MMC1 Phase 2 done (src/cartridge.rs rewired): mapper is Box<dyn Mapper>, 8KB prg_ram added, derives now Debug only, from_file accepts mapper 0/1, read/write delegate to mapper, mirroring() accessor + read/write_prg_ram
- MMC1 Phase 3 done (src/bus.rs): $6000-$7FFF routes to cart prg_ram, $8000-$FFFF writes route to cart.write_prg
- MMC1 Phase 4 done (regression gate): nestest still matches 5004 lines / 0 mismatches, builds and runs clean
- MMC1 Phase 5 partial (blargg harness in main.rs): cargo run = nestest mode; cargo run -- <rom> = blargg mode (cpu.reset, cycle cap, reports $6000-$6003). official_only.nes returns $6000=80 = blargg "test in progress" sentinel - the test waits for NMI (vblank) which we cannot produce without a PPU. Deferred until PPU/APU exist (blargg suite is the LAST milestone anyway)
- NMI/IRQ servicing done (src/cpu.rs): set_nmi/set_irq setters, shared interrupt() helper (push PC hi/lo, push p with B CLEAR unlike BRK, set I, jump vector, 7 cycles), NMI checked before IRQ at the top of step, IRQ masked by the I flag, cycles accumulated on both interrupt paths. cpu_interrupts.nes currently reports $6000=01 - it runs but needs the APU frame-counter IRQ source ($4017) to pass, which is roadmap Phase 6; verification deferred to the last milestone
- Backup library mapper audit: mappers 0 (NROM) and 1 (MMC1) cover many games; still need mapper 2 (UNROM: Castlevania, Contra, Megaman 1), mapper 4 (MMC3: SMB2, SMB3, Lolo 2), mapper 7 (AOROM: Who Framed Roger Rabbit)
- Finding: real commercial games rarely use illegal opcodes; none of the backup library needs them. Official-only CPU is sufficient for the goal of playing these games
## Next ## Next
CURRENT TASK: add MMC1 (mapper 1) support so blargg official_only.nes / all_instrs.nes can run (authoritative opcode check). Then the illegal-opcode phase for the full 8991-line nestest pass. REWEIGHTED ROADMAP: priority is now building the rest of the NES hardware so games actually run visibly, instead of chasing the full blargg/illegal-opcode pass. Goal: see the backup library running in a GUI. First test game: SMB (NROM, no new mapper needed). Blargg full suite + illegal opcodes are LAST, once the hardware can display test output on the NES screen.
MMC1 plan (new mapper.rs module, trait-based for future-proofing): Phases in order:
- Phase 1 - DONE (src/mapper.rs written): Mirroring enum (5 variants incl. OneScreenLow/High), trait Mapper: Debug (read_prg/write_prg/read_chr/write_chr/mirroring, ROM passed in per call), Nrom impl (16KB-mirror/32KB-direct, write_prg no-op), Mmc1 impl (shift-register protocol: bit-7 reset, shift LSB, latch after 5 by addr range; PRG modes 0/1=32K at $8000 bank=prg_bank&0x0E, 2/3=16K with $8000 fixed bank0 $C000 switches; CHR mode 0=8K via chr0, 1=two 4K via chr0/chr1 modulo-safe; init control=0x0C). Code verified by review; crate intentionally not compiling yet until Phase 2 rewiring 1. NMI/IRQ servicing - DONE (see Done section). cpu_interrupts.nes verification deferred to the last milestone (needs APU frame-counter IRQ)
- Phase 2 - DONE (src/cartridge.rs rewired): mapper is Box<dyn Mapper>, added 8KB prg_ram, derives now Debug only. from_file accepts mapper 0/1 and builds Nrom::new(mirroring) / Mmc1::new(). read/write_prg, read/write_chr delegate to mapper. Added mirroring() accessor + read/write_prg_ram (offset addr - 0x6000). mod mapper; added to main.rs 2. PPU-1 rendering core (new src/ppu.rs): registers $2000-$2007 routed in bus (currently stubbed 0), VRAM (nametables, pattern tables, palettes), background rendering into 256x240 framebuffer of palette indices, $2002 status with vblank flag, mirroring from cart, vblank raises NMI. Acceptance: SMB title screen renders, verified headless by framebuffer dump. NEXT
- Phase 3 - DONE (src/bus.rs): $6000-$7FFF reads route to cart.read_prg_ram, writes to cart.write_prg_ram (was stub 0 / dropped). $8000-$FFFF writes route to cart.write_prg (was dropped) 3. PPU-2 sprites + scrolling + $4014 OAM DMA: makes SMB actually playable
- Phase 4 - DONE (regression gate): nestest still matches 5004 lines / 0 mismatches. Crate builds and runs clean 4. GUI (egui + winit, first external deps): display framebuffer, 60fps loop, keyboard -> controller ($4016/$4017). First "games running on screen" moment
- Phase 5 - NEXT: Blargg harness in main.rs: cargo run = nestest mode (unchanged); cargo run -- <rom> = blargg mode using cpu.reset() (boots from $FFFC-$FFFD vector), run cycle cap, report $6000-$6003. Verify with official_only.nes 5. Mappers: 2 (UNROM), 7 (AOROM) simple; 4 (MMC3) complex - includes scanline IRQ counter required for SMB3 status bar. Verified by running the real backups
- Phase 6 - Tests: flip rejects_mmc1 to positive loads_mmc1, update mirroring assertions to accessor, add shift-register latching / PRG banking math / PRG RAM roundtrip / Nrom unit tests 6. APU audio: 5 channels, frame counter -> IRQ, mixing. Verified with SMB
7. LAST: blargg full suite (screen output becomes observable) + illegal-opcode phase for the full 8991-line nestest pass
Then after MMC1: Design note for PPU: PPU needs &mut Cartridge (CHR via read_chr/write_chr, mirroring) and &mut Cpu (raise NMI). Plan a main-frame loop that owns bus + cpu and drives both, rather than PPU holding references (borrow-checker friction). Framebuffer = palette indices (1 byte/pixel), RGB conversion happens in the GUI.
- Illegal-opcode phase: implement the undocumented opcodes (0x04 and friends) for the full 8991-line nestest pass. ~76 illegal opcodes present in the log, grouped into batches (23 undocumented NOPs, SLO/RLA/SRE/RRA shift-logic hybrids, LAX/SAX load-store combos, DCP/ISC RMW compare-subtract hybrids, EB = SBC imm alias). Reference: nesdev wiki undocumented opcode table
- Run blargg mapper-0 suite (coredump, cpu_timing_test, ram_after_reset, registers) Blargg/illegal background: ~76 illegal opcodes in the nestest log (23 undocumented NOPs, SLO/RLA/SRE/RRA, LAX/SAX, DCP/ISC, EB=SBC alias). Reference: nesdev wiki. Not needed for the backup library but wanted for the full log pass.
- Then PPU/bus/timing, GUI (egui + winit) come after the CPU milestone
## Decisions ## Decisions
@@ -67,4 +75,8 @@ Then after MMC1:
- Mapper 0 first, other mappers (MMC1, CNROM) implemented after CPU works - Mapper 0 first, other mappers (MMC1, CNROM) implemented after CPU works
- Mappers use a trait-based design (Mapper trait + Box<dyn Mapper>) so new mappers are self-contained impl blocks; ROM data owned by Cartridge and passed in per call - Mappers use a trait-based design (Mapper trait + Box<dyn Mapper>) so new mappers are self-contained impl blocks; ROM data owned by Cartridge and passed in per call
- PRG RAM lives on the Cartridge (8KB), the bus interconnects it - PRG RAM lives on the Cartridge (8KB), the bus interconnects it
- MMC1 (mapper 1) done for the cartridge; mappers 2/4/7 planned after PPU
- NMI/IRQ servicing done; APU frame counter (IRQ source for cpu_interrupts.nes) comes with the APU milestone
- Priority reweighted: build the rest of the NES hardware (PPU -> GUI -> mappers -> APU) so games run visibly; blargg full suite + illegal opcodes are last
- Framebuffer is palette indices (1 byte/pixel) from the PPU; RGB conversion happens in the GUI
- Test ROMs are homebrew/open, no ethics issue; game backups stay in gitignored roms/backup - Test ROMs are homebrew/open, no ethics issue; game backups stay in gitignored roms/backup
+27
View File
@@ -92,8 +92,35 @@ impl Cpu {
self.read(bus, 0x0100 + self.sp as u16) self.read(bus, 0x0100 + self.sp as u16)
} }
// public API for other hardware
pub fn set_nmi(&mut self) { self.nmi_pending = true; }
pub fn set_irq(&mut self) { self.irq_pending = true; }
// shared interrumpt function
fn interrupt(&mut self, bus: &mut impl Bus, vector: u16) -> u8 {
self.push(bus, (self.pc >> 8) as u8);
self.push(bus, (self.pc & 0xFF) as u8);
self.push(bus, self.p); // B clear (unlike BRK)
self.set_flag(FLAG_INTERRUPT);
self.pc = self.read16(bus, vector);
7
}
// Step shell // Step shell
pub fn step(&mut self, bus: &mut impl Bus) -> u8 { pub fn step(&mut self, bus: &mut impl Bus) -> u8 {
if self.nmi_pending {
self.nmi_pending = false;
let cycles = self.interrupt(bus, 0xFFFA);
self.cycles += cycles as u64;
return cycles;
}
if self.irq_pending && !self.flag(FLAG_INTERRUPT) {
self.irq_pending = false;
let cycles = self.interrupt(bus, 0xFFFE);
self.cycles += cycles as u64;
return cycles;
}
let opcode: u8 = self.fetch(bus); let opcode: u8 = self.fetch(bus);
let cycles = match opcode { let cycles = match opcode {
// Jumps & subroutines // Jumps & subroutines