finished phase 2a
This commit is contained in:
@@ -52,14 +52,21 @@ REWEIGHTED ROADMAP: priority is now building the rest of the NES hardware so gam
|
|||||||
Phases in order:
|
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)
|
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
|
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:
|
||||||
|
|
||||||
|
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
|
||||||
3. PPU-2 sprites + scrolling + $4014 OAM DMA: makes SMB actually playable
|
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
|
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
|
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
|
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
|
7. LAST: blargg full suite (screen output becomes observable) + illegal-opcode phase for the full 8991-line nestest pass
|
||||||
|
|
||||||
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.
|
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).
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ mod cartridge;
|
|||||||
mod bus;
|
mod bus;
|
||||||
mod cpu;
|
mod cpu;
|
||||||
mod mapper;
|
mod mapper;
|
||||||
|
mod palette;
|
||||||
|
mod render;
|
||||||
|
mod ppu;
|
||||||
|
|
||||||
use crate::bus::{Bus, NesBus};
|
use crate::bus::{Bus, NesBus};
|
||||||
use crate::cartridge::Cartridge;
|
use crate::cartridge::Cartridge;
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
pub const SYSTEM_PALETTE: [(u8, u8, u8); 64] = [
|
||||||
|
(0x5D, 0x5D, 0x5D), (0x00, 0x00, 0x80), (0x1D, 0x00, 0x96), (0x49, 0x00, 0x8C), // 00-03
|
||||||
|
(0x72, 0x00, 0x66), (0x89, 0x00, 0x21), (0x84, 0x12, 0x00), (0x66, 0x2E, 0x00), // 04-07
|
||||||
|
(0x3A, 0x49, 0x00), (0x00, 0x56, 0x00), (0x00, 0x50, 0x18), (0x00, 0x3E, 0x37), // 08-0B
|
||||||
|
(0x00, 0x2B, 0x55), (0x00, 0x2B, 0x66), (0x00, 0x24, 0x54), (0x00, 0x16, 0x2E), // 0C-0F
|
||||||
|
(0xBE, 0xBE, 0xBE), (0x00, 0x24, 0xF6), (0x51, 0x14, 0xE7), (0x93, 0x0C, 0xD4), // 10-13
|
||||||
|
(0xBF, 0x0B, 0x9F), (0xD8, 0x20, 0x4C), (0xD0, 0x46, 0x00), (0xA8, 0x6A, 0x00), // 14-17
|
||||||
|
(0x73, 0x8A, 0x00), (0x13, 0xA5, 0x00), (0x00, 0x9A, 0x32), (0x00, 0x7B, 0x67), // 18-1B
|
||||||
|
(0x00, 0x62, 0x8D), (0x00, 0x5C, 0xA1), (0x00, 0x4F, 0x85), (0x00, 0x37, 0x50), // 1C-1F
|
||||||
|
(0xFF, 0xFF, 0xFF), (0x64, 0x8D, 0xFF), (0xA0, 0x70, 0xFF), (0xD8, 0x5A, 0xFF), // 20-23
|
||||||
|
(0xFF, 0x4E, 0xFF), (0xFF, 0x63, 0x90), (0xFF, 0x82, 0x3C), (0xFF, 0xA0, 0x28), // 24-27
|
||||||
|
(0xCD, 0xB8, 0x19), (0x72, 0xD4, 0x00), (0x49, 0xCA, 0x6B), (0x27, 0xB0, 0x9B), // 28-2B
|
||||||
|
(0x2D, 0x95, 0xC7), (0x3A, 0x8B, 0xE0), (0x27, 0x76, 0xB7), (0x00, 0x5B, 0x83), // 2C-2F
|
||||||
|
(0xFF, 0xFF, 0xFF), (0xB1, 0xC7, 0xFF), (0xCC, 0xB6, 0xFF), (0xE2, 0xA8, 0xFF), // 30-33
|
||||||
|
(0xFF, 0x9F, 0xFF), (0xFF, 0xA9, 0xBE), (0xFF, 0xB4, 0x8C), (0xFF, 0xBE, 0x7A), // 34-37
|
||||||
|
(0xEC, 0xC6, 0x6F), (0xB8, 0xD6, 0x5E), (0x9F, 0xD2, 0x8A), (0x8E, 0xC5, 0xA6), // 38-3B
|
||||||
|
(0x92, 0xB9, 0xBF), (0x99, 0xB4, 0xC9), (0x86, 0xA6, 0xB1), (0x66, 0x87, 0x96), // 3C-3F
|
||||||
|
]; // as per the 2C02 color palette
|
||||||
|
|
||||||
|
pub fn index_to_rgb(index: u8) -> (u8, u8, u8) {
|
||||||
|
SYSTEM_PALETTE[(index & 0x3F) as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_color_values() {
|
||||||
|
// spot-check the 2C02 table: a few entries across the 4 rows
|
||||||
|
assert_eq!(SYSTEM_PALETTE[0x00], (0x5D, 0x5D, 0x5D)); // dark gray
|
||||||
|
assert_eq!(SYSTEM_PALETTE[0x0F], (0x00, 0x16, 0x2E)); // dark blue
|
||||||
|
assert_eq!(SYSTEM_PALETTE[0x10], (0xBE, 0xBE, 0xBE)); // medium gray
|
||||||
|
assert_eq!(SYSTEM_PALETTE[0x20], (0xFF, 0xFF, 0xFF)); // white
|
||||||
|
assert_eq!(SYSTEM_PALETTE[0x21], (0x64, 0x8D, 0xFF)); // light blue
|
||||||
|
assert_eq!(SYSTEM_PALETTE[0x3F], (0x66, 0x87, 0x96)); // dark muted blue
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn index_to_rgb_masks_to_six_bits() {
|
||||||
|
// palette indices are 6-bit; anything >= 0x40 wraps around safely
|
||||||
|
assert_eq!(index_to_rgb(0x00), index_to_rgb(0x40));
|
||||||
|
assert_eq!(index_to_rgb(0x21), index_to_rgb(0x61));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use crate::palette::index_to_rgb;
|
||||||
|
|
||||||
|
pub trait Renderer {
|
||||||
|
fn present(&mut self, framebuffer: &[u8], width: u16, height: u16);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PpmRenderer {
|
||||||
|
dir: String,
|
||||||
|
frame: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl PpmRenderer {
|
||||||
|
pub fn new(dir: &str) -> Self {
|
||||||
|
Self { dir: dir.to_string(), frame: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Renderer for PpmRenderer {
|
||||||
|
fn present(&mut self, framebuffer: &[u8], width: u16, height: u16) {
|
||||||
|
std::fs::create_dir_all(&self.dir).expect("failed to create render dir");
|
||||||
|
let path = format!("{}/frame_{:04}.ppm", self.dir, self.frame);
|
||||||
|
std::fs::write(&path, ppm_bytes(framebuffer, width, height))
|
||||||
|
.expect("failed to write PPM file");
|
||||||
|
self.frame += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ppm_bytes(framebuffer: &[u8], width: u16, height: u16) -> Vec<u8> {
|
||||||
|
let mut out = format!("P6\n{width} {height}\n255\n").into_bytes();
|
||||||
|
for &idx in framebuffer {
|
||||||
|
let (r, g, b) = index_to_rgb(idx);
|
||||||
|
out.extend_from_slice(&[r, g, b]);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ppm_bytes_has_correct_header() {
|
||||||
|
let fb = [0u8; 4];
|
||||||
|
let bytes = ppm_bytes(&fb, 2, 2);
|
||||||
|
let expected_header = "P6\n2 2\n255\n";
|
||||||
|
let got = String::from_utf8_lossy(&bytes[..expected_header.len()]);
|
||||||
|
assert_eq!(got, expected_header);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ppm_bytes_emits_rgb_per_pixel() {
|
||||||
|
let fb = [0x21u8, 0x21, 0x00, 0x3F]; // 4 pixels: 2x2
|
||||||
|
let bytes = ppm_bytes(&fb, 2, 2);
|
||||||
|
let header = format!("P6\n2 2\n255\n");
|
||||||
|
let pixels = &bytes[header.len()..];
|
||||||
|
assert_eq!(pixels.len(), 2 * 2 * 3); // 12 RGB bytes
|
||||||
|
|
||||||
|
assert_eq!(&pixels[0..3], &[0x64, 0x8D, 0xFF]); // pixel 0 = 0x21, unchanged
|
||||||
|
assert_eq!(&pixels[9..12], &[0x66, 0x87, 0x96]); // pixel 3 = 0x3F (was 6..9)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user