broken PPU imp

This commit is contained in:
2026-08-09 19:46:07 -05:00
parent 2964e6fad2
commit d758fae787
5 changed files with 245 additions and 24 deletions
+11 -8
View File
@@ -42,6 +42,8 @@
- 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
- 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
@@ -52,21 +54,22 @@ REWEIGHTED ROADMAP: priority is now building the rest of the NES hardware so gam
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 (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. Confirmed decisions: full dot-accurate scanline model (sprites in PPU-2 need cycle timing, MMC3 needs scanline IRQ), scroll registers $2005/$2006 correct now (v/t/x/w loopy system), Cartridge passed as a parameter to read/write/tick (no Rc/RefCell). Build steps:
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:
2a. DONE - palette.rs (64-color 2C02 SYSTEM_PALETTE in index order + index_to_rgb with & 0x3F mask) and render.rs (trait Renderer with present(framebuffer, width, height) + PpmRenderer + pure ppm_bytes for testability). 4 unit tests pass (known palette values, index masking, PPM header, per-pixel RGB routing). Note: 2C02 chosen over composite palettes - we render the raw PPU output. src/ppu.rs exists as an empty placeholder
2b. Ppu struct + registers $2000-$2007 + VRAM routing ($0000-$1FFF = cart CHR via cart.read_chr/write_chr, $2000-$2FFF = nametables in vram[0x800] with cart.mirroring(), $3F00-$3FFF = palette[32] with $3F10-$3F1F mirror) + bus wiring ($2000-$3FFF reads/writes via disjoint-field borrows). Verify with a debug harness writing $2006/$2007 and reading back. NEXT
2c. Frame timing + vblank + NMI: 341 dots/scanline, 262 scanlines/frame, 3 PPU dots per CPU cycle. tick(cycles) advances cycles*3 dots. Scanline 241 dot 0 sets vblank flag + nmi_pending. Frame loop bridges: take_nmi() -> cpu.set_nmi(). Verify $2002 flag toggles and NMI fires
2d. Static background render at scroll 0: nametable -> attribute -> pattern -> palette into 256x240 framebuffer. May show messy SMB output before scroll lands - expected
2e. Scroll registers v/t/x/w (loopy system) + the dot-by-dot background pipeline: shift registers (bg_pattern_lo/hi 16-bit, bg_attr 16-bit, tile_latch, attr_latch), 4 fetch stages per 8-dot group (nametable, attribute, pattern low, pattern high), coarse X increment every 8 dots with nametable flip, coarse Y/fine Y wrap at scanline end. The densest code in the project
2f. Acceptance: SMB title screen renders recognizable, dumped to PPM
2c. Full background renderer (merged timing + scroll + pipeline):
- Ppu gains: scanline (0-261), cycle (0-340), framebuffer[256*240] palette indices, nmi_pending, frame_complete, loopy registers t (temp addr) + x (fine X) + w (write latch), background pipeline state (bg_pattern_lo/hi u16 shift regs, bg_attr u16, tile_latch, attr_latch)
- Timing: tick(cart, cycles) advances cycle by cycles*3; cycle>=341 -> scanline++; odd-frame skip on pre-render line (261 is 340 dots on odd frames); scanline 241 dot 0 sets vblank + nmi_pending; scanline 261 dot 0 clears vblank; frame_complete at 261 end. Rendering on 0-239 and 261 (pre-render fetches, no output); 240 idle
- Loopy scroll: $2005 first: t=(t&0x7FE0)|(val>>3), x=val&7, w=true; $2005 second: t=(t&0x0C1F)|((val&0xF8)<<2)|((val&7)<<12), w=false; $2006 first: t=(t&0x00FF)|((val&0x3F)<<8), w=true; $2006 second: t=(t&0x7F00)|val, v=t, w=false; $2002 read resets w
- Background fetch pipeline (4 fetch / 8 dot cycle): dot%8==0 nametable byte -> tile_latch, ==2 attribute -> attr_latch, ==4 pattern lo from CHR, ==6 pattern hi, ==7 reload shift regs + increment coarse X (31->0 flips nametable X bit). Each dot outputs pixel combining bg_pattern_lo/hi top bit + attr bit for 2-bit palette select -> background palette -> framebuffer. Vertical increment at scanline end (fine Y 7->0, coarse Y 29->0 flips nametable Y bit)
- main.rs: add use crate::render::{PpmRenderer, Renderer}, define frames (e.g. 3) in run_cart, cargo run -- <rom> boots from reset and renders frames to renders/. run_cart already calls the real methods (tick/frame_done/take_nmi/framebuffer/begin_frame)
- Acceptance (single gate): cargo run -- roms/backup/SMB.nes -> renders/frame_0000.ppm recognizable SMB title screen
3. PPU-2 sprites + scrolling + $4014 OAM DMA: makes SMB actually playable
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).
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.