diff --git a/src/bin/siena/main.rs b/src/bin/siena/main.rs index d8ba1a0..5fc0565 100644 --- a/src/bin/siena/main.rs +++ b/src/bin/siena/main.rs @@ -10,6 +10,7 @@ use siena::frontend::sdl::{SDLEventPump, SDLRenderer}; use siena::frontend::Renderer; use siena::snes::bus::mainbus::{BusTrace, Mainbus}; use siena::snes::bus::Bus; +use siena::snes::cartridge::Cartridge; use siena::snes::cpu_65816::cpu::Cpu65816; use siena::snes::joypad::{Button, Joypad, JoypadEvent}; use siena::snes::ppu::{SCREEN_HEIGHT, SCREEN_WIDTH}; @@ -69,14 +70,6 @@ fn main() -> Result<()> { let mut args = Args::parse(); let f = fs::read(args.filename)?; - let load_offset = match f.len() % 1024 { - 0 => 0, - 0x200 => { - println!("Cartridge contains 0x200 bytes of weird header"); - 0x200 - } - _ => panic!("Illogical cartridge file size: 0x{:08X}", f.len()), - }; let (mut joypads, joypad_senders) = Joypad::new_channel_all(); for j in joypads.iter_mut() { @@ -84,7 +77,9 @@ fn main() -> Result<()> { } let display = SDLRenderer::new(SCREEN_WIDTH, SCREEN_HEIGHT)?; let eventpump = SDLEventPump::new(); - let bus = Mainbus::::new(&f[load_offset..], args.bustrace, display, joypads); + let cart = Cartridge::load(&f); + println!("Cartridge: {}", &cart); + let bus = Mainbus::::new(cart, args.bustrace, display, joypads); let reset = bus.read16(0xFFFC); let mut cpu = Cpu65816::>::new(bus, reset); diff --git a/src/snes/bus/mainbus.rs b/src/snes/bus/mainbus.rs index 6a3e6a8..3bc97c1 100644 --- a/src/snes/bus/mainbus.rs +++ b/src/snes/bus/mainbus.rs @@ -5,6 +5,7 @@ use dbg_hex::dbg_hex; use crate::frontend::Renderer; use crate::snes::bus::{Address, Bus, BusMember}; +use crate::snes::cartridge::Cartridge; use crate::snes::joypad::{Joypad, JOYPAD_COUNT}; use crate::snes::ppu::PPU; use crate::tickable::{Tickable, Ticks}; @@ -28,7 +29,7 @@ pub struct Mainbus where TRenderer: Renderer, { - cartridge: Vec, + cartridge: Cartridge, wram: Vec, pub trace: BusTrace, @@ -220,13 +221,13 @@ where TRenderer: Renderer, { pub fn new( - cartridge: &[u8], + cartridge: Cartridge, trace: BusTrace, renderer: TRenderer, joypads: [Joypad; JOYPAD_COUNT], ) -> Self { Self { - cartridge: cartridge.to_owned(), + cartridge, wram: vec![0; WRAM_SIZE], trace, dma: [DMAChannel::new(); DMA_CHANNELS], @@ -549,19 +550,14 @@ where _ => None, } } - // WS1/2 LoROM - 0x8000..=0xFFFF => Some(self.cartridge[addr - 0x8000 + (bank & !0x80) * 0x8000]), + // LoROM + 0x8000..=0xFFFF => self.cartridge.read(fulladdr), _ => None, }, - // WS1 HiROM - 0x40..=0x7D => { - Some(self.cartridge[(addr + ((bank - 0x40) * 0x10000)) % self.cartridge.len()]) - } + 0x40..=0x7D => self.cartridge.read(fulladdr), // Full WRAM area 0x7E..=0x7F => Some(self.wram[((bank - 0x7E) * WRAM_BANK_SIZE) + addr]), - // WS2 HiROM - 0xC0..=0xFF => Some(self.cartridge[addr + ((bank - 0xC0) * 0x10000)]), _ => None, }; @@ -735,6 +731,7 @@ where _ => None, }, + 0x70..=0x7D => self.cartridge.write(fulladdr, val), // Full WRAM area 0x7E..=0x7F => Some(self.wram[((bank - 0x7E) * WRAM_BANK_SIZE) + addr] = val), @@ -819,7 +816,7 @@ mod tests { fn mainbus() -> Mainbus { let (joypads, _) = Joypad::new_channel_all(); Mainbus::::new( - &[], + Cartridge::new_empty(), BusTrace::All, NullRenderer::new(0, 0).unwrap(), joypads, diff --git a/src/snes/cartridge.rs b/src/snes/cartridge.rs new file mode 100644 index 0000000..066a349 --- /dev/null +++ b/src/snes/cartridge.rs @@ -0,0 +1,213 @@ +use std::fmt; + +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +use crate::snes::bus::{Address, BusMember}; + +const HDR_TITLE_OFFSET: usize = 0x00; +const HDR_TITLE_SIZE: usize = 21; +const HDR_MAPMODE_OFFSET: usize = 0x15; +const HDR_CHIPSET_OFFSET: usize = 0x16; +const HDR_ROMSIZE_OFFSET: usize = 0x17; +const HDR_RAMSIZE_OFFSET: usize = 0x18; +const HDR_CHECKSUM_OFFSET: usize = 0x1C; +const HDR_ICHECKSUM_OFFSET: usize = 0x1E; +const HDR_LEN: usize = 0x1F; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, FromPrimitive)] +pub enum Chipset { + RomOnly = 0, + RomRam = 1, + RomRamBat = 2, + RomCo = 3, + RomRamCo = 4, + RomRamCoBat = 5, + RomCoBat = 6, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, FromPrimitive)] +pub enum MapMode { + LoROM = 0, + HiROM = 1, + ExHiROM = 5, +} + +/// A mounted SNES cartridge +pub struct Cartridge { + rom: Vec, + ram: Vec, + header_offset: usize, + + /// True if HiROM. Cache this for performance reasons. + hirom: bool, +} + +impl Cartridge { + /// Returns the title of the cartridge as set in the header. + pub fn get_title(&self) -> String { + String::from_utf8( + self.rom[(self.header_offset + HDR_TITLE_OFFSET) + ..(self.header_offset + HDR_TITLE_OFFSET + HDR_TITLE_SIZE)] + .into_iter() + .take_while(|&&c| c != 0) + .copied() + .collect(), + ) + .unwrap_or("INVALID".to_string()) + .trim() + .to_owned() + } + + fn get_map(&self) -> MapMode { + MapMode::from_u8(self.rom[self.header_offset + HDR_MAPMODE_OFFSET] & 0x0F).unwrap() + } + + fn get_chipset(&self) -> Chipset { + Chipset::from_u8(self.rom[self.header_offset + HDR_CHIPSET_OFFSET] & 0x0F).unwrap() + } + + fn get_rom_size(&self) -> usize { + (1 << self.rom[self.header_offset + HDR_ROMSIZE_OFFSET]) * 1024 + } + + fn get_ram_size(&self) -> usize { + (1 << self.rom[self.header_offset + HDR_RAMSIZE_OFFSET]) * 1024 + } + + fn probe_header(hdr: &[u8]) -> bool { + let csum1: u16 = + (hdr[HDR_CHECKSUM_OFFSET + 0] as u16) | (hdr[HDR_CHECKSUM_OFFSET + 1] as u16) << 8; + let csum2: u16 = + (hdr[HDR_ICHECKSUM_OFFSET + 0] as u16) | (hdr[HDR_ICHECKSUM_OFFSET + 1] as u16) << 8; + return csum1 == (csum2 ^ 0xFFFF); + } + + /// Loads a cartridge. + /// Fails if it cannot find the cartridge header. + pub fn load(rom: &[u8]) -> Self { + Self::load_with_save(rom, &[]) + } + + /// Loads a cartridge and a save. + /// Fails if it cannot find the cartridge header. + pub fn load_with_save(rom: &[u8], _save: &[u8]) -> Self { + let load_offset = match rom.len() % 1024 { + 0 => 0, + 0x200 => { + println!("Cartridge contains 0x200 bytes of weird header"); + 0x200 + } + _ => panic!("Illogical cartridge file size: 0x{:08X}", rom.len()), + }; + let rom = &rom[load_offset..]; + + let mut header_offset = None; + for possible_offset in [0x7FC0, 0xFFC0] { + if (possible_offset + HDR_LEN) > rom.len() { + continue; + } + if Self::probe_header(&rom[possible_offset..]) { + println!("Cartridge header at 0x{:06X}", possible_offset); + header_offset = Some(possible_offset); + break; + } + } + + let mut c = Self { + rom: Vec::from(rom), + ram: vec![0; 512 * 1024], + hirom: false, + header_offset: header_offset.expect("Could not locate header"), + }; + c.hirom = match c.get_map() { + MapMode::HiROM => true, + _ => false, + }; + c + } + + /// Loads a cartridge but does not do header detection + pub fn load_nohdr(rom: &[u8], hirom: bool) -> Self { + Self { + rom: Vec::from(rom), + ram: vec![0; 512 * 1024], + hirom, + header_offset: 0, + } + } + + /// Creates an empty new cartridge (for tests) + /// Does not do header detection + pub fn new_empty() -> Self { + Self { + rom: vec![], + ram: vec![0; 512 * 1024], + hirom: false, + header_offset: 0, + } + } +} + +impl fmt::Display for Cartridge { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "\"{}\" - {:?} {:?} - {} KB ROM, {} KB RAM", + self.get_title(), + self.get_chipset(), + self.get_map(), + self.get_rom_size() / 1024, + self.get_ram_size() / 1024, + ) + } +} + +impl BusMember for Cartridge { + fn read(&self, fulladdr: Address) -> Option { + let (bank, addr) = ((fulladdr >> 16) as usize, (fulladdr & 0xFFFF) as usize); + + match (bank, addr) { + // LoROM + (0x00..=0x3F | 0x80..=0xBF, 0x8000..=0xFFFF) => { + Some(self.rom[addr - 0x8000 + (bank & !0x80) * 0x8000]) + } + + // HiROM SRAM + (0x30..=0x3F, 0x6000..=0x6FFF) if self.hirom => { + Some(self.ram[(bank - 0x30) * 0x1000 + (addr - 0x6000)]) + } + + // HiROM + (0x40..=0x6F, _) => Some(self.rom[(addr + ((bank - 0x40) * 0x10000)) % self.rom.len()]), + + // LoROM SRAM + (0x70..=0x7D, 0x0000..=0x7FFF) if !self.hirom => { + Some(self.ram[(bank - 0x70) * 0x8000 + addr]) + } + + // HiROM + (0xC0..=0xFF, _) => Some(self.rom[addr + ((bank - 0xC0) * 0x10000)]), + + _ => None, + } + } + + fn write(&mut self, fulladdr: Address, val: u8) -> Option<()> { + let (bank, addr) = ((fulladdr >> 16) as usize, (fulladdr & 0xFFFF) as usize); + + match (bank, addr) { + // HiROM SRAM + (0x30..=0x3F, 0x6000..=0x6FFF) if self.hirom => { + Some(self.ram[(bank - 0x30) * 0x1000 + (addr - 0x6000)] = val) + } + + // LoROM SRAM + (0x70..=0x7D, 0x0000..=0x7FFF) if !self.hirom => { + Some(self.ram[(bank - 0x70) * 0x8000 + addr] = val) + } + + _ => None, + } + } +} diff --git a/src/snes/mod.rs b/src/snes/mod.rs index 3f08ab4..630a329 100644 --- a/src/snes/mod.rs +++ b/src/snes/mod.rs @@ -1,4 +1,5 @@ pub mod bus; +pub mod cartridge; pub mod cpu_65816; pub mod joypad; pub mod ppu; diff --git a/src/test/mod.rs b/src/test/mod.rs index 1b5e8f6..30f99e5 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -8,6 +8,7 @@ use std::time::Instant; use crate::frontend::test::TestRenderer; use crate::snes::bus::mainbus::{BusTrace, Mainbus}; use crate::snes::bus::Bus; +use crate::snes::cartridge::Cartridge; use crate::snes::cpu_65816::cpu::Cpu65816; use crate::snes::joypad::Joypad; use crate::snes::ppu::{SCREEN_HEIGHT, SCREEN_WIDTH}; @@ -15,7 +16,8 @@ use crate::snes::ppu::{SCREEN_HEIGHT, SCREEN_WIDTH}; fn test_display(rom: &[u8], pass_hash: &[u8], time_limit: u128, stable: bool) { let (display, dispstatus) = TestRenderer::new_test(SCREEN_WIDTH, SCREEN_HEIGHT); let (joypads, _) = Joypad::new_channel_all(); - let bus = Mainbus::::new(rom, BusTrace::None, display, joypads); + let cart = Cartridge::load_nohdr(rom, false); + let bus = Mainbus::::new(cart, BusTrace::None, display, joypads); let reset = bus.read16(0xFFFC); let mut cpu = Cpu65816::>::new(bus, reset);