Files
rnes/STATE.md
T
2026-08-09 23:36:45 -05:00

108 lines
19 KiB
Markdown

# 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)
- 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
- PPU 2a done (src/palette.rs + src/render.rs): 64-color 2C02 SYSTEM_PALETTE in index order, index_to_rgb with & 0x3F mask. Renderer trait (the only hardware trait) + PpmRenderer writing numbered frames to renders/ (frame_0000.ppm etc.) + pure ppm_bytes for testability. 4 unit tests pass. 2C02 chosen over composite palettes - we render raw PPU output
- PPU 2b done (src/ppu.rs struct + registers + VRAM routing + src/bus.rs open-bus): Ppu has ctrl/mask/status/oam/oamaddr, shared write_latch (W bit for $2005/$2006), vram_addr (14-bit), data_buffer ($2007 read-buffer), vram[0x1000] (4KB for FourScreen), palette[32]. read/write handle $2000-$2007 with open-bus for write-only regs ($2000/$2001/$2003/$2005/$2006), $2002 read = (status & 0xE0) | (open_bus & 0x1F) + clears vblank + resets write latch, $2004 write increments oamaddr, $2007 palette reads bypass the read-buffer. VRAM routing: $0000-$1FFF = CHR via cart, $2000-$3EFF = nametables via cart.mirroring() (H/V/OneScreenLow/High/FourScreen), $3F00-$3FFF = palette with 10/14/18/1C mirror. Bus tracks open_bus (last byte on CPU data bus, updated on every read/write) and passes it into ppu.read; PPU regs routed via disjoint-field borrows; APU/expansion stubs now return open_bus instead of 0. cartridge.rs mirroring honors the four-screen header bit
- PPU 2c implementation done (src/ppu.rs full background renderer): scanline/cycle timing (3 PPU dots per CPU cycle, 341 dots/scanline, 262 scanlines/frame, odd-frame 340-dot pre-render line), vblank+NMI at scanline 241 dot 0, clear at 261 dot 0, frame_complete at 261 end. Loopy scroll $2005/$2006 (t/x/w, shared write latch reset on $2002 read). Background fetch pipeline (4-fetch/8-dot: nametable at %8==0, attribute at 2, pattern lo at 4, pattern hi at 6, reload shift regs + increment coarse X at 7), two attribute shift registers (0xFF/0x00 per bit), horizontal copy at dot 255, vertical increment at 256, vertical copy at 280. Fine-X pre-shift at cycle 0 of visible scanlines. Transparent pixel uses palette[0]. Fixed mirroring swap (Horizontal=>a>>11, Vertical=>a>>10). Bus frame-driving methods (tick_ppu/frame_done/take_nmi/framebuffer/begin_frame). main.rs run_cart boots any ROM, renders frames to renders/ with PpmRenderer
- PPU 2c FIXED - SMB BACKGROUND RENDERS CORRECTLY. Three bugs found and fixed, in order:
1. NMI fired unconditionally at vblank (src/ppu.rs dot()): the PPU asserted nmi_pending on every vblank regardless of the $2000 NMI-enable bit (ctrl & 0x80). Game boots with NMI off, so every frame the CPU got yanked into the NMI handler mid-boot, pushing stack and spinning in the RAM-clear loop at $90CC (SP dropped 8 bytes/frame, PC pinned at $90DE, palette/nametable never written = all grey). Fixed by gating nmi_pending on ctrl & 0x80. Verified with debug_out/state.txt + pctrace.txt (src/dump.rs dbg mode)
2. Attribute table fetch wrong (src/ppu.rs bg_fetch): column term used (v & 0x07) instead of ((v >> 2) & 0x07), and quadrant selector used coarseX bit 0 / coarseY bit 0 instead of bit 1 ((v & 0x40) >> 4 | (v & 0x02)). Produced green/brown logo colors, invisible letters (blank lines through P/R), and wrong-palette garbage on the right edge. Fixed both formulas per nesdev
3. Background pipeline timing (src/ppu.rs dot/bg_fetch, modeled after olcNES Part 4 reference): shifters must shift ONLY during fetch windows (cycles 2-257 and 321-337), with LoadBackgroundShifters + TransferAddressX at cycle 257 (preloads the NEXT scanline's first tile), IncrementScrollY at 256, idle NT fetches at 338/340, TransferAddressY on the pre-render line 280-304. Our code shifted every dot and copied horizontal at 255 -> each scanline started mid-tile, image shifted showing page 1 + page 2. Rewrote dot() with windowed shifters, (cycle-1)%8 fetch phases, load_shifters/update_shifters helpers, bit-mux fine-X (0x8000 >> x) in render_bg_pixel. Frame now renders pixel-correct with correct colors
- ACCEPTANCE MET: SMB renders the full frame correctly (background). Mario himself is a sprite and is NOT rendered yet - that's the next milestone (PPU-2 sprites + $4014 OAM DMA)
- PPU-2 3a SPRITES DONE: $4014 OAM DMA (bus.rs routes to Ppu::oam_dma, copies 256 bytes from CPU RAM page through the 2KB mirror), sprite evaluation at cycle 257 (8 sprites per scanline, overflow flag bit 5, sprite-0 tracking), sprite pattern fetch at cycle 340 (8x8 via ctrl bit 3, 8x16 via tile id bit 0, V-flip with 7-row inversion, H-flip via reverse_bits, hi plane +8), per-sprite shifters with X countdown (decrement X until 0 then shift, cycles 1-257), and render_pixel compositing (bg vs sprite priority via attr bit 5, sprites use palettes 4-7, sprite-0 hit sets status bit 6 at cycle>=9). Fixed one integration bug: the old bg-only pipeline block was left in dot() duplicating the new combined block (shifters shifted/fetched twice per dot) - deleted the old block. ACCEPTANCE MET: Mario visible and the coin sprite animates across frames
- PPU-2 3b INPUT DONE (src/input.rs): InputSource trait (pluggable sources), MaskInput concrete source, Controller struct with strobe-latch + shift-out protocol ($4016/$4017, |0x80 filler so bits 9+ read 1), Key enum + key_to_button (Z=A, X=B, Shift=Select, Enter=Start, Arrows=D-pad). Bus wired. 3 unit tests pass. Fixed match-ordering bug: controller arms were below the $4000-$401F range arm (unreachable); specific arms must come before overlapping ranges
- 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
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.
Phases in order:
1. NMI/IRQ servicing - DONE (see Done section). cpu_interrupts.nes verification deferred to the last milestone (needs APU frame-counter IRQ)
2. PPU-1 rendering core - PARTIAL: 2a (palette/render) and 2b (registers/VRAM/open-bus) DONE (see Done section). Remaining: full background rendering in ONE merged step - timing + scroll + background pipeline together, no intermediate gating (we do not test until the full PPU exists). Acceptance: SMB title screen renders to PPM via main.rs run_cart. NEXT. Confirmed decisions: full dot-accurate scanline model, loopy v/t/x/w scroll system, Cartridge passed as parameter, open-bus tracking in the bus, hardware-accurate throughout (same philosophy as the CPU). Merged step:
2c. Full background renderer - DONE. Acceptance MET: SMB renders correctly (colors correct, no shift, no holes). Mario missing = sprites not yet implemented
2d. DONE - diagnosis + fix. Built src/dump.rs dbg mode (state.txt + pctrace.txt per frame). Found 3 bugs (see Done section): unconditional NMI, attribute address/quadrant formulas, and pipeline timing (shift windows + cycle-257 preload). All fixed, verified by renders/ output
3. NEXT: PPU-2 sprites + input + GUI window - makes SMB actually playable (Mario + enemies visible AND moveable). Detailed plan below. Four phases, sprites first (verifiable headless via PPM + dbg), then input logic (testable in isolation), then the window (first external deps):
3a. SPRITES - DONE (see Done section). Acceptance MET: Mario + coin sprite visible, coin animates
3b. INPUT - DONE (see Done section). src/input.rs: InputSource trait + MaskInput + Controller (strobe latch, shift-out on read, |0x80 filler so bits 9+ read 1) + Key enum + key_to_button mapping. 3 unit tests pass (strobe repeats A, A-first shift-out order, B-second). Bus wired: $4016/$4017 read + write_strobe; caught a match-ordering bug (controller arms were below the 0x4000..=0x401F range arm so unreachable - specific arms must come first). $4017 APU frame-counter bits ignored for now (no APU yet)
3c. BUS WIRING - DONE (done together with 3b): Controller in NesBus, $4016/$4017 read = shift out one bit, write = strobe. Open-bus handling for unused controller bits deferred
3d. NEXT: GUI WINDOW (src/render.rs + new deps): first external crates (egui + egui-winit + winit). main.rs owns the frame loop for ALL modes (nestest/blargg/cart/dbg/gui) - window is passive, renderer is dumb:
- Renderer trait gains two default no-op methods (PPM inherits them): fn should_close(&self) -> bool { false }, fn poll_input(&mut self, controller: &mut Controller) {}
- WindowRenderer: egui + winit impl of Renderer. present() converts palette indices -> RGB via index_to_rgb, uploads as texture, draws. poll_input() maps winit key events -> Controller bits. should_close() from winit close request
- Keyboard defaults: Z=A, X=B, Shift=Select, Enter=Start, Arrows=D-pad (key_to_button already in input.rs, winit KeyCode -> Key glue needed)
- PPM stays as headless/dbg output; renders/ no longer written for normal GUI runs (was ~1MB/frame, heavy)
- New run_gui <rom> mode: event pump -> one emulated frame -> present, 60fps
- Acceptance: walk Mario in the window, screen scrolls correctly between nametables (this is how scrolling gets verified - the scroll logic itself is already fixed and correct)
4. GUI (egui + winit, first external deps): display framebuffer, 60fps loop, keyboard -> controller ($4016/$4017). First "games running on screen" moment
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
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
Design note for PPU: PPU takes &mut Cartridge as a parameter to read/write/tick (CHR via cart.read_chr/write_chr, mirroring via cart.mirroring). Frame loop owns bus + cpu + renderer and drives all three - loop bridges NMI via ppu.take_nmi() -> cpu.set_nmi(), so neither PPU nor bus holds a &mut Cpu. Framebuffer = palette indices (1 byte/pixel), RGB conversion happens in the Renderer (PPM now, GUI later). Open bus tracked in the bus (last byte on CPU data bus, updated on every read/write), passed into ppu.read; write-only PPU regs return it. Hardware accuracy throughout - no accuracy questions pending, we build it correct like the CPU.
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.
## Decisions
- No external dependencies yet, keeping it pure std until the GUI milestone
- GUI choice: egui + winit (decided, not yet used); main.rs owns the frame loop for all modes, window is passive
- InputSource is a trait (Box<dyn InputSource>) so input sources are pluggable: keyboard, gamepad, bot/test harness. Controller (shift-register protocol) is a concrete struct - the protocol is fixed hardware, only the button source varies
- 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<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
- 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