Files
rnes/STATE.md
T

8.5 KiB

Status

Done

  • Repo set up, backed up the original game ROMs to roms/backup (gitignored, stays local)
  • Got a full set of CPU test ROMs in roms/test/cpu: kevtris nestest (with reference log) plus the blargg suite (instr tests, timing, interrupts, dummy reads, reset checks)
  • Cartridge module done and working (src/cartridge.rs):
    • iNES header parser (magic, PRG/CHR sizes, mapper, mirroring, flags)
    • Mapper 0 (NROM) support only: 16KB PRG mirroring, 32KB direct, CHR-ROM and CHR-RAM (zeroed 8KB buffer)
    • read_prg/read_chr/write_chr with debug_assert guards
    • Split from_file (thin I/O wrapper) / parse_data (pure parser) for testability
    • 8 unit tests, all passing (header, PRG mapping, error cases, CHR-RAM roundtrip)
  • Bus module done (src/bus.rs):
    • Bus trait (read/write) so the CPU is decoupled from the concrete memory layout
    • NesBus: 2KB mirrored CPU RAM, cartridge PRG routing, PPU/APU/SRAM/expansion regions stubbed (return 0)
  • CPU skeleton done (src/cpu.rs):
    • Flag bit constants, registers (A/X/Y/SP/PC/P), set/clear/flag helpers, set_zp_flags
    • Memory helpers: fetch, fetch16, read, write, read16, push, pop
    • All 10 addressing mode helpers, including the JMP ($xxxx) page-wrap bug and page-cross cycle penalties
    • First 3 opcodes implemented (JMP abs, LDX imm, STX zp)
    • step dispatch shell + trace in nestest format
    • set_test_start for the nestest harness
  • nestest harness working (main.rs): loads nestest.nes, runs from $C000 with CYC:7, dumps trace to ours.log
  • Batch 1 done (subroutines + stack): JSR, RTS, PHA, PLA, PHP, PLP, RTI, BRK, plus NOP and SEC as quick wins. Verified SP:FD->FB across JSR (push high-then-low works)
  • Batch 2 done (branches): all 8 branches (BEQ/BNE/BCC/BCS/BPL/BMI/BVC/BVS) via one shared branch() routine with condition param. Taken = +3 cycles, page-cross = +1 more. Verified both taken and not-taken paths in the log
  • Flag set/clear ops done (part of Batch 10): CLC, CLI, CLV, CLD, SEI, SED (SEC done in Batch 1). SEI/SED set, the rest clear. Fixed a real bug where SEI/SED were clearing instead of setting
  • Batch 8 done (inc/dec): INC (4 modes), DEC (4 modes), INX, INY, DEX, DEY. RMW cores (inc/dec) read-modify-write and correctly return base WITHOUT the page-cross extra (RMW cycles are fixed). Register ops are wrapping_add/sub with Z/N only. 124 of 151 official opcodes done. INY/INX carried the log from 506 to 678 lines
  • Batch 9 done (shifts): ASL, LSR, ROL, ROR (accumulator + 4 memory modes each) via four shift cores (shift_asl/lsr/rol/ror) + wrappers. ROL/ROR read carry-in before overwriting it. Memory shifts follow the RMW fixed-cycle rule. 144 of 151
  • Batch 10 done (transfers + JMP ind): TAX, TAY, TXA, TYA, TSX, TXS (all 2 cycles; TXS sets NO flags, the other five set Z/N), plus JMP (0x6C) which finally uses address_ind. Resolved the long-standing address_ind dead-code warning
  • OFFICIAL CPU COMPLETE: all 151 official opcodes implemented. Trace matches the nestest reference log through line 5003 (entire official section), 0 mismatches. Stops at line 5004 (0x04, first illegal opcode) by design
  • Fixed a latent Batch 4 bug the log caught at line 3319: the sta core added the page-cross extra, but stores have FIXED cycle counts. Fixed sta core to return base without extra, and corrected bases (sta_abx/sta_aby 5, sta_izy 6). Before the fix the diff had a +1 CYC drift from line 3319 onward
  • Batch 3 done (loads): LDA (8 modes), LDX (5 modes), LDY (5 modes) via three shared cores (lda/ldx/ldy) + thin wrappers. Caught two real bugs in review: all 4 LDY wrappers called self.ldx (would write to X instead of Y), and 6 of 8 LDX/LDY wrappers had wrong base cycles (3 instead of 4). Both fixed
  • Batch 4 done (stores): STA (7 modes), STX (3 modes), STY (3 modes) via three shared cores (sta/stx/sty) + thin wrappers. No flag changes. Refactored stx_zp to use the shared stx core for consistency. Fixed a comment typo (sta_abx was labeled 0x95 instead of 0x9D)
  • Batch 5 done (compares): CMP (8 modes), CPX (3 modes), CPY (3 modes) via three shared cores (cmp/cpx/cpy) + wrappers + immediate variants. Sets C (reg >= mem), Z, N; no store. Uses wrapping_sub for the borrow math. 70 of 151 official opcodes done. No diff progress expected until the log reaches these opcodes
  • Batch 6 done (arithmetic): ADC (8 modes), SBC (8 modes), BIT (2 modes) via shared add_with_carry core + adc/sbc/bit cores + wrappers. SBC adds the one's complement (A + ~mem + C), which makes the V-flag math correct. Decimal flag inert (binary only). 88 of 151 official opcodes done. Big diff progress: BIT/PHP/PLA/PHA carried the log from 38 to 73 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
  • trace bytes are length-aware via opcode_len (fixed-width byte field, columns align for awk diff)

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.

MMC1 plan (new mapper.rs module, trait-based for future-proofing):

  • 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
  • Phase 2 - DONE (src/cartridge.rs rewired): mapper is Box, 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
  • 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)
  • Phase 4 - DONE (regression gate): nestest still matches 5004 lines / 0 mismatches. Crate builds and runs clean
  • Phase 5 - NEXT: Blargg harness in main.rs: cargo run = nestest mode (unchanged); cargo run -- = blargg mode using cpu.reset() (boots from $FFFC-$FFFD vector), run cycle cap, report $6000-$6003. Verify with official_only.nes
  • 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

Then after MMC1:

  • 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)
  • Then PPU/bus/timing, GUI (egui + winit) come after the CPU milestone

Decisions

  • No external dependencies yet, keeping it pure std until the GUI milestone
  • GUI choice: egui + winit (decided, not yet used)
  • Scope: cartridge + CPU first, verified headless before graphics
  • Bus is a trait (impl Bus) so the CPU works against any memory layout (real bus, test bus, debug bus)
  • Flags stored as a raw u8 with named bit-mask constants (FLAG_CARRY etc.)
  • opcode length is a growing match in opcode_len() (returns u8); migrate to a full 256-entry table when it gets big
  • trace diff compares PC + A/X/Y/P/SP/CYC, ignoring disassembly/PPU columns
  • Mapper 0 first, other mappers (MMC1, CNROM) implemented after CPU works
  • Mappers use a trait-based design (Mapper trait + Box) 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
  • Test ROMs are homebrew/open, no ethics issue; game backups stay in gitignored roms/backup