made mapper module to support the various NES cartridge types in the furyte, MMC1 support added
This commit is contained in:
@@ -39,11 +39,19 @@
|
|||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
Official CPU milestone is COMPLETE (all 151 opcodes, nestest-verified through line 5003). The full 8991-line nestest log tests illegal opcodes too (225 distinct opcodes total; first illegal is 0x04 at line 5004). Official opcodes never seen in the log: BRK 0x00, CLI 0x58.
|
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.
|
||||||
|
|
||||||
Follow-up options, in any order:
|
MMC1 plan (new mapper.rs module, trait-based for future-proofing):
|
||||||
- Illegal-opcode phase: implement the undocumented opcodes (0x04 and friends) for the full 8991-line nestest pass. ~76 illegal opcodes present in the log. Reference: nesdev wiki undocumented opcode table
|
|
||||||
- MMC1 (mapper 1) in the cartridge so blargg official_only.nes / all_instrs.nes can run (authoritative official check)
|
- 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<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
|
||||||
|
- 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 -- <rom> = 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)
|
- 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
|
- Then PPU/bus/timing, GUI (egui + winit) come after the CPU milestone
|
||||||
|
|
||||||
@@ -57,4 +65,6 @@ Follow-up options, in any order:
|
|||||||
- opcode length is a growing match in opcode_len() (returns u8); migrate to a full 256-entry table when it gets big
|
- 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
|
- 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
|
- 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
|
||||||
- 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
|
||||||
|
|||||||
+3
-1
@@ -23,7 +23,7 @@ impl Bus for NesBus {
|
|||||||
0x2000..=0x3FFF => 0, //PPU registers stub, temporal
|
0x2000..=0x3FFF => 0, //PPU registers stub, temporal
|
||||||
0x4000..=0x401F => 0, //APU controller stub, temporal
|
0x4000..=0x401F => 0, //APU controller stub, temporal
|
||||||
0x4020..=0x5FFF => 0, //Expansion ROM stub. temporal
|
0x4020..=0x5FFF => 0, //Expansion ROM stub. temporal
|
||||||
0x6000..=0x7FFF => 0, //SRAM stub, no reading saves for you :3
|
0x6000..=0x7FFF => self.cart.read_prg_ram(address), // SRAM reads from the cartridge
|
||||||
0x8000..=0xFFFF => self.cart.read_prg(address), // this is te space PRG Rom gets "loaded" to in real hardware
|
0x8000..=0xFFFF => self.cart.read_prg(address), // this is te space PRG Rom gets "loaded" to in real hardware
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,8 @@ impl Bus for NesBus {
|
|||||||
fn write(&mut self, address: u16, value: u8) {
|
fn write(&mut self, address: u16, value: u8) {
|
||||||
match address {
|
match address {
|
||||||
0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize] = value,
|
0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize] = value,
|
||||||
|
0x6000..=0x7FFF => self.cart.write_prg_ram(address, value),
|
||||||
|
0x8000..=0xFFFF => self.cart.write_prg(address, value),
|
||||||
_ => {} // Everything else dropped for now
|
_ => {} // Everything else dropped for now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-16
@@ -1,15 +1,14 @@
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
use crate::mapper::{Mapper, Mirroring, Nrom, Mmc1};
|
||||||
pub enum Mirroring { Horizontal, Vertical, FourScreen }
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug)]
|
||||||
pub struct Cartridge {
|
pub struct Cartridge {
|
||||||
prg_rom: Vec<u8>,
|
prg_rom: Vec<u8>,
|
||||||
chr_rom: Vec<u8>,
|
chr_rom: Vec<u8>,
|
||||||
mirroring: Mirroring,
|
prg_ram: Vec<u8>, // 8KB PRG RAM (battery-backed SRAM lives on the cartridge)
|
||||||
battery: bool,
|
battery: bool,
|
||||||
trainer: bool,
|
trainer: bool,
|
||||||
four_screen: bool,
|
four_screen: bool,
|
||||||
mapper: u8,
|
mapper: Box<dyn Mapper>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cartridge {
|
impl Cartridge {
|
||||||
@@ -40,11 +39,13 @@ impl Cartridge {
|
|||||||
let four_screen: bool = flags6 & 0b1000 != 0; // this is meant to override mirroring but both will be stored
|
let four_screen: bool = flags6 & 0b1000 != 0; // this is meant to override mirroring but both will be stored
|
||||||
let mapper: u8 = ((flags7 & 0xF0) >> 4) << 4 | ((flags6 & 0xF0) >> 4);
|
let mapper: u8 = ((flags7 & 0xF0) >> 4) << 4 | ((flags6 & 0xF0) >> 4);
|
||||||
|
|
||||||
// Gate for Mapper as we are going NROM-only for now
|
// Gate for Mapper: NROM (0) and MMC1 (1) supported for now
|
||||||
|
|
||||||
if mapper != 0 {
|
let mapper: Box<dyn Mapper> = match mapper {
|
||||||
return Err(format!("mapper {mapper} not supported yet, this is temporal"));
|
0 => Box::new(Nrom::new(mirroring)),
|
||||||
}
|
1 => Box::new(Mmc1::new()),
|
||||||
|
_ => return Err(format!("mapper {mapper} not supported yet, this is temporal")),
|
||||||
|
};
|
||||||
|
|
||||||
// Compute the ROM offsets for future use
|
// Compute the ROM offsets for future use
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ impl Cartridge {
|
|||||||
Ok(Cartridge {
|
Ok(Cartridge {
|
||||||
prg_rom: data[prg_start..prg_start + prg_len].to_vec(),
|
prg_rom: data[prg_start..prg_start + prg_len].to_vec(),
|
||||||
chr_rom: if chr_len == 0 { vec![0u8; 0x2000] } else { data[chr_start..chr_start + chr_len].to_vec() }, // Vector of 8KB filled with 0's or the actual CHR ROM data
|
chr_rom: if chr_len == 0 { vec![0u8; 0x2000] } else { data[chr_start..chr_start + chr_len].to_vec() }, // Vector of 8KB filled with 0's or the actual CHR ROM data
|
||||||
mirroring,
|
prg_ram: vec![0u8; 0x2000],
|
||||||
battery,
|
battery,
|
||||||
trainer,
|
trainer,
|
||||||
four_screen,
|
four_screen,
|
||||||
@@ -73,22 +74,31 @@ impl Cartridge {
|
|||||||
// read_* operations are for the CPU to work with, letting it read the ROM chips on the emulated cartridge
|
// read_* operations are for the CPU to work with, letting it read the ROM chips on the emulated cartridge
|
||||||
pub fn read_prg(&self, address: u16) -> u8 {
|
pub fn read_prg(&self, address: u16) -> u8 {
|
||||||
debug_assert!(address >= 0x8000, "PRG read outside of $8000-$FFFF; Address from CPU: {address:#06X}"); // #06X makes the Address readable similar to $1FFF
|
debug_assert!(address >= 0x8000, "PRG read outside of $8000-$FFFF; Address from CPU: {address:#06X}"); // #06X makes the Address readable similar to $1FFF
|
||||||
let mut offset: u16 = address - 0x8000;
|
self.mapper.read_prg(&self.prg_rom, address)
|
||||||
if self.prg_rom.len() == 0x4000 {
|
|
||||||
offset %= 0x4000;
|
|
||||||
}
|
}
|
||||||
self.prg_rom[offset as usize]
|
pub fn write_prg(&mut self, address: u16, value: u8) {
|
||||||
|
self.mapper.write_prg(address, value);
|
||||||
}
|
}
|
||||||
pub fn read_chr(&self, address: u16) -> u8 {
|
pub fn read_chr(&self, address: u16) -> u8 {
|
||||||
debug_assert!(address < 0x2000, "CHR reads outside $0000-$1FFF; Address from CPU: {address:#06X}");
|
debug_assert!(address < 0x2000, "CHR reads outside $0000-$1FFF; Address from CPU: {address:#06X}");
|
||||||
self.chr_rom[address as usize]
|
self.mapper.read_chr(&self.chr_rom, address)
|
||||||
}
|
}
|
||||||
pub fn write_chr(&mut self, address: u16, value: u8) {
|
pub fn write_chr(&mut self, address: u16, value: u8) {
|
||||||
debug_assert!(address < 0x2000, "CHR writes outside $0000-$1FFF; Address from CPU: {address:#06X}");
|
debug_assert!(address < 0x2000, "CHR writes outside $0000-$1FFF; Address from CPU: {address:#06X}");
|
||||||
self.chr_rom[address as usize] = value
|
self.mapper.write_chr(&mut self.chr_rom, address, value);
|
||||||
|
}
|
||||||
|
pub fn read_prg_ram(&self, address: u16) -> u8 {
|
||||||
|
self.prg_ram[(address - 0x6000) as usize]
|
||||||
|
}
|
||||||
|
pub fn write_prg_ram(&mut self, address: u16, value: u8) {
|
||||||
|
self.prg_ram[(address - 0x6000) as usize] = value;
|
||||||
|
}
|
||||||
|
pub fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mapper.mirroring()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -158,3 +168,4 @@ mod tests {
|
|||||||
assert_eq!(cart.read_chr(0x1234), 0xAB);
|
assert_eq!(cart.read_chr(0x1234), 0xAB);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
mod cartridge;
|
mod cartridge;
|
||||||
mod bus;
|
mod bus;
|
||||||
mod cpu;
|
mod cpu;
|
||||||
|
mod mapper;
|
||||||
|
|
||||||
use crate::bus::{Bus, NesBus};
|
use crate::bus::{Bus, NesBus};
|
||||||
use crate::cartridge::Cartridge;
|
use crate::cartridge::Cartridge;
|
||||||
|
|||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Mirroring {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
OneScreenLow, // MMC1 control bits 0-1 = 0
|
||||||
|
OneScreenHigh, // MMC1 control bits 0-1 = 1
|
||||||
|
FourScreen,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Mapper: Debug {
|
||||||
|
fn read_prg(&self, prg: &[u8], addr: u16) -> u8;
|
||||||
|
fn write_prg(&mut self, addr: u16, val: u8);
|
||||||
|
fn read_chr(&self, chr: &[u8], addr: u16) -> u8;
|
||||||
|
fn write_chr(&mut self, chr: &mut [u8], addr: u16, val: u8);
|
||||||
|
fn mirroring(&self) -> Mirroring;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Nrom { mirroring: Mirroring }
|
||||||
|
|
||||||
|
impl Nrom {
|
||||||
|
pub fn new(mirroring: Mirroring) -> Self {
|
||||||
|
Self { mirroring }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Nrom {
|
||||||
|
fn read_prg(&self, prg: &[u8], addr: u16) -> u8 {
|
||||||
|
debug_assert!(addr >= 0x8000, "PRG read outside of $8000-$FFFF; Address from CPU: {addr:#06X}"); // #06X makes the Address readable similar to $1FFF
|
||||||
|
let mut offset = addr - 0x8000;
|
||||||
|
if prg.len() == 0x4000 { offset %= 0x4000 } // 16 KiB prg mirror
|
||||||
|
prg[offset as usize]
|
||||||
|
}
|
||||||
|
fn write_prg(&mut self, _addr: u16, _val: u8) { /* NROM has no registers */ }
|
||||||
|
fn read_chr(&self, chr: &[u8], addr: u16) -> u8 {
|
||||||
|
chr[ addr as usize ]
|
||||||
|
}
|
||||||
|
fn write_chr(&mut self, chr: &mut [u8], addr: u16, val: u8) {
|
||||||
|
debug_assert!(addr < 0x2000, "CHR writes outside $0000-$1FFF; Address from CPU: {addr:#06X}");
|
||||||
|
chr[ addr as usize ] = val;
|
||||||
|
}
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Mmc1 {
|
||||||
|
shift_reg: u8, shift_count: u8, control: u8, chr0: u8,
|
||||||
|
chr1: u8, prg_bank:u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mmc1 {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { shift_reg: 0, shift_count: 0, control: 0x0C, chr0: 0, chr1: 0, prg_bank: 0 }
|
||||||
|
}
|
||||||
|
// shared CHR bank math for read/write; mode = control>>3 & 1
|
||||||
|
fn chr_offset(&self, chr: &[u8], addr: u16) -> usize {
|
||||||
|
if (self.control >> 3) & 1 == 0 {
|
||||||
|
// mode 0: 8K switch via chr0 (bit 0 ignored, bank even)
|
||||||
|
let count = (chr.len() / 0x2000).max(1);
|
||||||
|
let bank = ((self.chr0 & 0x0E) as usize) % count;
|
||||||
|
bank * 0x2000 + addr as usize
|
||||||
|
} else if addr < 0x1000 {
|
||||||
|
// mode 1, low 4K: chr0
|
||||||
|
let count = (chr.len() / 0x1000).max(1);
|
||||||
|
let bank = (self.chr0 as usize) % count;
|
||||||
|
bank * 0x1000 + addr as usize
|
||||||
|
} else {
|
||||||
|
// mode 1, high 4K: chr1
|
||||||
|
let count = (chr.len() / 0x1000).max(1);
|
||||||
|
let bank = (self.chr1 as usize) % count;
|
||||||
|
bank * 0x1000 + (addr - 0x1000) as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Mmc1 {
|
||||||
|
// Shift-Register protocol
|
||||||
|
fn write_prg(&mut self, addr: u16, val: u8) {
|
||||||
|
if val & 0x80 != 0 {
|
||||||
|
self.shift_reg = 0;
|
||||||
|
self.shift_count = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.shift_reg = (self.shift_reg >> 1) | ((val & 1) << 4);
|
||||||
|
self.shift_count += 1;
|
||||||
|
if self.shift_count == 5 {
|
||||||
|
match addr {
|
||||||
|
0x8000..=0x9FFF => self.control = self.shift_reg,
|
||||||
|
0xA000..=0xBFFF => self.chr0 = self.shift_reg,
|
||||||
|
0xC000..=0xDFFF => self.chr1 = self.shift_reg,
|
||||||
|
_ => self.prg_bank = self.shift_reg,
|
||||||
|
}
|
||||||
|
self.shift_reg = 0;
|
||||||
|
self.shift_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRG banking
|
||||||
|
fn read_prg(&self, prg: &[u8], addr: u16) -> u8 {
|
||||||
|
let banks = prg.len() / 0x4000; // 16k bank count
|
||||||
|
let mode = (self.control >> 2) & 0x3;
|
||||||
|
let offset = if mode <= 1 {
|
||||||
|
// 32K at $8000
|
||||||
|
let bank = ((self.prg_bank & 0x0E) as usize) % banks;
|
||||||
|
bank * 0x4000 + (addr - 0x8000) as usize
|
||||||
|
} else if addr < 0xC000 {
|
||||||
|
// 16K mode at $8000
|
||||||
|
(addr - 0x8000) as usize
|
||||||
|
} else {
|
||||||
|
// 16K mode at $C000
|
||||||
|
let bank = self.prg_bank as usize % banks;
|
||||||
|
bank * 0x4000 + (addr - 0xC000) as usize
|
||||||
|
};
|
||||||
|
prg[offset]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHR banking
|
||||||
|
fn read_chr(&self, chr: &[u8], addr: u16) -> u8 { chr[self.chr_offset(chr, addr)] }
|
||||||
|
fn write_chr(&mut self, chr: &mut [u8], addr: u16, val: u8) { let o = self.chr_offset(chr, addr); chr[o] = val; }
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
match self.control & 0x3 {
|
||||||
|
0 => Mirroring::OneScreenLow,
|
||||||
|
1 => Mirroring::OneScreenHigh,
|
||||||
|
2 => Mirroring::Vertical,
|
||||||
|
_ => Mirroring::Horizontal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user