iNes 2.0 ROM loading

This commit is contained in:
FyloZ 2025-01-17 22:18:08 -05:00
parent 86463f59cd
commit 223fb6f55d
Signed by untrusted user who does not match committer: william
GPG Key ID: 835378AE9AF4AE97
6 changed files with 401 additions and 40 deletions

View File

@ -2,7 +2,7 @@ use crate::cpu::op::AddressingMode::*;
use crate::cpu::op::{AddressingMode, Instruction}; use crate::cpu::op::{AddressingMode, Instruction};
use crate::cpu::Cpu; use crate::cpu::Cpu;
impl Cpu { impl Cpu<'_> {
pub fn disassemble_next_instr(&self) { pub fn disassemble_next_instr(&self) {
let registers = self.registers; let registers = self.registers;
let op_code = self.memory_bus.get_byte(registers.pc); let op_code = self.memory_bus.get_byte(registers.pc);

View File

@ -53,13 +53,13 @@ struct CpuRegisters {
bitflags! { bitflags! {
pub struct CpuStatus: u8 { pub struct CpuStatus: u8 {
const Carry = 0b00000001; const Carry = 0b0000_0001;
const Zero = 0b00000010; const Zero = 0b0000_0010;
const InterruptDisable = 0b00000100; const InterruptDisable = 0b0000_0100;
const Decimal = 0b00001000; const Decimal = 0b0000_1000;
const Break = 0b00010000; const Break = 0b0001_0000;
const Overflow = 0b01000000; const Overflow = 0b0100_0000;
const Negative = 0b10000000; const Negative = 0b1000_0000;
} }
} }
@ -131,8 +131,8 @@ pub trait CpuInternals {
fn stack_pop_context(&mut self); fn stack_pop_context(&mut self);
} }
impl Cpu { impl<'a> Cpu<'a> {
pub fn new<'a>(memory_bus: &mut MemoryBus) -> Self { pub fn new(memory_bus: &mut MemoryBus) -> Self {
Cpu { Cpu {
registers: CpuRegisters { registers: CpuRegisters {
pc: 0x8000, pc: 0x8000,
@ -151,7 +151,7 @@ impl Cpu {
} }
} }
impl CpuInternals for Cpu { impl CpuInternals for Cpu<'_> {
fn get_status_flag(&self, flag: CpuStatus) -> bool { fn get_status_flag(&self, flag: CpuStatus) -> bool {
*self.registers.status & flag *self.registers.status & flag
} }
@ -240,7 +240,7 @@ impl CpuInternals for Cpu {
} }
} }
impl Clock for Cpu { impl Clock for Cpu<'_> {
fn cycle(&mut self) { fn cycle(&mut self) {
self.cycle += 1; self.cycle += 1;

View File

@ -97,7 +97,7 @@ impl Operand {
} }
// Operands // Operands
impl Cpu { impl Cpu<'_> {
/// 16x16 grid of 6502 opcodes. Matches datasheet matrix for easy lookup /// 16x16 grid of 6502 opcodes. Matches datasheet matrix for easy lookup
#[rustfmt::skip] #[rustfmt::skip]
pub const INSTRUCTIONS: [Instruction; 256] = [ pub const INSTRUCTIONS: [Instruction; 256] = [

View File

@ -7,7 +7,7 @@ fn is_sign_overflow(val1: u8, val2: u8, result: u8) -> bool {
(val1 & 0x80 == val2 & 0x80) && (val1 & 0x80 != result & 0x80) (val1 & 0x80 == val2 & 0x80) && (val1 & 0x80 != result & 0x80)
} }
impl Cpu { impl Cpu<'_> {
pub fn exec_instruction(&mut self, instr: Instruction) { pub fn exec_instruction(&mut self, instr: Instruction) {
let operand = self.operand_decode(instr.addr_mode()); let operand = self.operand_decode(instr.addr_mode());
if operand.is_page_crossing { if operand.is_page_crossing {

View File

@ -1,37 +1,278 @@
//! INes 2.0 Format implementation //! INes 2.0 Format implementation
pub struct INesRom {} use crate::rom::{CpuTiming, NametableMirroring, Rom, RomLoader, RomReadError};
use bitflags::bitflags;
struct INesHeader { pub struct INesHeader {
prg_rom_size: u16, prg_rom_size_lsb: u8,
chr_rom_size: u16, chr_rom_size_lsb: u8,
// Flags 6
// D~7654 3210
// ---------
// NNNN FTBM
// |||| |||+-- Hard-wired nametable layout
// |||| ||| 0: Vertical arrangement ("mirrored horizontally") or mapper-controlled
// |||| ||| 1: Horizontal arrangement ("mirrored vertically")
// |||| ||+--- "Battery" and other non-volatile memory
// |||| || 0: Not present
// |||| || 1: Present
// |||| |+--- 512-byte Trainer
// |||| | 0: Not present
// |||| | 1: Present between Header and PRG-ROM data
// |||| +---- Alternative nametables
// |||| 0: No
// |||| 1: Yes
// ++++------ Mapper Number D3..D0
flag_6: u8, flag_6: u8,
// Flags 7
// D~7654 3210
// ---------
// NNNN 10TT
// |||| ||++- Console type
// |||| || 0: Nintendo Entertainment System/Family Computer
// |||| || 1: Nintendo Vs. System
// |||| || 2: Nintendo Playchoice 10
// |||| || 3: Extended Console Type
// |||| ++--- NES 2.0 identifier
// ++++------ Mapper Number D7..D4
flag_7: u8, flag_7: u8,
mapper: u8,
sub_mapper: u8, // Mapper MSB/Submapper
prg_ram_size: u8, // D~7654 3210
prg_nvram_size: u8, // ---------
// SSSS NNNN
// |||| ++++- Mapper number D11..D8
// ++++------ Submapper number
mapper_msb_submapper: u8,
// PRG-ROM/CHR-ROM size MSB
// D~7654 3210
// ---------
// CCCC PPPP
// |||| ++++- PRG-ROM size MSB
// ++++------ CHR-ROM size MSB
rom_sizes_msb: u8,
// PRG-RAM/EEPROM size
// D~7654 3210
// ---------
// pppp PPPP
// |||| ++++- PRG-RAM (volatile) shift count
// ++++------ PRG-NVRAM/EEPROM (non-volatile) shift count
// If the shift count is zero, there is no PRG-(NV)RAM.
// If the shift count is non-zero, the actual size is
// "64 << shift count" bytes, i.e. 8192 bytes for a shift count of 7.
prg_ram_eeprom_size: u8,
// CHR-RAM size
// D~7654 3210
// ---------
// cccc CCCC
// |||| ++++- CHR-RAM size (volatile) shift count
// ++++------ CHR-NVRAM size (non-volatile) shift count
// If the shift count is zero, there is no CHR-(NV)RAM.
// If the shift count is non-zero, the actual size is
// "64 << shift count" bytes, i.e. 8192 bytes for a shift count of 7.
chr_ram_size: u8, chr_ram_size: u8,
cpu_trainer: CpuTimingMode,
misc_rom_count: u8, // CPU/PPU Timing
default_expansion_device: u8, // vs_ppu_type: u8, // D~7654 3210
// vs_hardware_type: u8, // ---------
// .... ..VV
// ++- CPU/PPU timing mode
// 0: RP2C02 ("NTSC NES")
// 1: RP2C07 ("Licensed PAL NES")
// 2: Multiple-region
// 3: UA6538 ("Dendy")
cpu_ppu_timing: u8,
// When Byte 7 AND 3 =1: Vs. System Type
// D~7654 3210
// ---------
// MMMM PPPP
// |||| ++++- Vs. PPU Type
// ++++------ Vs. Hardware Type
//
// When Byte 7 AND 3 =3: Extended Console Type
// D~7654 3210
// ---------
// .... CCCC
// ++++- Extended Console Type
system_type: u8,
// Miscellaneous ROMs
// D~7654 3210
// ---------
// .... ..RR
// ++- Number of miscellaneous ROMs present
misc_roms: u8,
// Default Expansion Device
// D~7654 3210
// ---------
// ..DD DDDD
// ++-++++- Default Expansion Device
default_expansion_device: u8,
} }
#[repr(u8)] impl INesHeader {
enum CpuTimingMode { /// Identification characters at the start of a iNes2.0 ROM file - NES<EOF>
NTSC = 0, const IDENTIFICATION: [u8; 4] = [0x4E, 0x45, 0x53, 0x1A];
PAL = 1,
Multiple = 2, fn get_prg_ram_size(&self) -> (usize, usize) {
Dendy = 3, let prg_ram_size = (self.prg_ram_eeprom_size & 0xF) as usize;
let prg_nvram_size = (self.prg_ram_eeprom_size >> 4) as usize;
(prg_ram_size, prg_nvram_size)
}
fn get_prg_rom_size(&self) -> usize {
let prg_rom_size_lsb = self.prg_rom_size_lsb as usize;
let prg_rom_size_msb = (self.rom_sizes_msb & 0x0F) as usize;
if prg_rom_size_msb < 0xF {
// LSB and MSB are the size of the PRG ROM in 16KiB units
((prg_rom_size_msb << 8) | prg_rom_size_lsb) << 14
} else {
// (2 ^ exponent) * multiplier
let multiplier = (prg_rom_size_lsb & 0x2) * 2 + 1;
let exponent = prg_rom_size_lsb >> 2;
(1 << exponent) * multiplier
}
}
fn get_chr_ram_size(&self) -> (usize, usize) {
let chr_ram_shifts = (self.chr_ram_size & 0xF) as usize;
let chr_nvram_shifts = (self.chr_ram_size >> 4) as usize;
let chr_ram_size = match chr_ram_shifts {
0 => 0,
shifts => 64 << shifts,
};
let chr_nvram_size = match chr_nvram_shifts {
0 => 0,
shifts => 64 << shifts,
};
(chr_ram_size, chr_nvram_size)
}
fn get_chr_rom_size(&self) -> usize {
let chr_rom_size_lsb = self.chr_rom_size_lsb as usize;
let chr_rom_size_msb = (self.rom_sizes_msb >> 4) as usize;
if chr_rom_size_msb < 0xF {
// LSB and MSB are the size of the CHR ROM in 8KiB units
((chr_rom_size_msb << 8) | chr_rom_size_lsb) << 13
} else {
// (2 ^ exponent) * multiplier
let multiplier = (chr_rom_size_lsb & 0x2) * 2 + 1;
let exponent = chr_rom_size_lsb >> 2;
(1 << exponent) * multiplier
}
}
fn get_trainer_size(&self) -> usize {
if self.flag_6 & 4 == 0 {
0
} else {
512
}
}
fn get_nametable_mirroring(&self) -> NametableMirroring {
match self.flag_6 & 1 {
0 => NametableMirroring::Horizontal,
_ => NametableMirroring::Vertical,
}
}
fn get_cpu_timing(&self) -> Result<CpuTiming, RomReadError> {
Ok(match self.cpu_ppu_timing {
0 => CpuTiming::Ntsc,
1 => CpuTiming::Pal,
2 => CpuTiming::Multiple,
3 => CpuTiming::Dendy,
_ => {
return Err(RomReadError::InvalidHeader(format!(
"Invalid CPU timing mode: {}",
self.cpu_ppu_timing
)))
}
})
}
fn parse(header: &[u8]) -> Self {
Self {
prg_rom_size_lsb: header[4],
chr_rom_size_lsb: header[5],
flag_6: header[6],
flag_7: header[7],
mapper_msb_submapper: header[8],
rom_sizes_msb: header[9],
prg_ram_eeprom_size: header[10],
chr_ram_size: header[11],
cpu_ppu_timing: header[12],
system_type: header[13],
misc_roms: header[14],
default_expansion_device: header[15],
}
}
} }
union CpuSystemType { impl RomLoader for INesHeader {
vs_system_type: VsSystemType, fn format_matches(header: &[u8]) -> bool {
extended_console_type: u8, for i in 0..4 {
} if header[i] != Self::IDENTIFICATION[i] {
// This is not a header from a iNes 2.0 ROM
return false;
}
}
struct VsSystemType { // if header[7] & 0b00001100 != 0b00001000 {
ppu_type: u8, // // This is a iNes 1.0 ROM
hardware_type: u8, // }
true
}
fn load(header: &[u8], file_data: &[u8]) -> Result<Rom, RomReadError> {
let ines_header = Self::parse(header);
let prg_rom_size = ines_header.get_prg_rom_size();
let chr_rom_size = ines_header.get_chr_rom_size();
let trainer_size = ines_header.get_trainer_size();
let rom_size = file_data.len();
let required_size = prg_rom_size + chr_rom_size + trainer_size;
if rom_size < required_size {
return Err(RomReadError::RomLengthMismatch(required_size, rom_size));
}
let (prg_ram_size, prg_nvram_size) = ines_header.get_prg_ram_size();
let (chr_ram_size, chr_nvram_size) = ines_header.get_chr_ram_size();
let nametable_mirroring = ines_header.get_nametable_mirroring();
let cpu_timing = ines_header.get_cpu_timing()?;
let prg_rom = file_data[trainer_size..prg_rom_size]
.to_vec()
.into_boxed_slice();
let chr_rom = file_data
[(trainer_size + prg_rom_size)..(trainer_size + prg_rom_size + chr_rom_size)]
.to_vec()
.into_boxed_slice();
Ok(Rom {
prg_ram_size,
prg_nvram_size,
prg_rom_size,
prg_rom,
chr_ram_size,
chr_nvram_size,
chr_rom_size,
chr_rom,
nametable_mirroring,
cpu_timing,
})
}
} }

View File

@ -1 +1,121 @@
mod ines; use crate::rom::ines::INesHeader;
use simplelog::{debug, info};
use std::fmt::{Display, Formatter};
use std::fs;
mod ines;
pub struct Rom {
/// The size of the program RAM in bytes
pub prg_ram_size: usize,
/// The size of the non-volatile program RAM in bytes
pub prg_nvram_size: usize,
/// The size of the program ROM in bytes
pub prg_rom_size: usize,
/// The program ROM
pub prg_rom: Box<[u8]>,
/// The size of the character RAM
pub chr_ram_size: usize,
/// The size of the character RAM (non-volatile)
pub chr_nvram_size: usize,
/// The size of the character ROM in bytes
pub chr_rom_size: usize,
/// The character ROM
pub chr_rom: Box<[u8]>,
/// The mirroring of the nametable
pub nametable_mirroring: NametableMirroring,
/// The timing of the CPU, also the region of the ROM
pub cpu_timing: CpuTiming,
}
pub enum NametableMirroring {
Horizontal,
Vertical,
}
pub enum CpuTiming {
Ntsc,
Multiple,
Pal,
Dendy
}
pub trait RomLoader {
/// Checks if a header is for this file format.
///
/// # Arguments
/// * `header` - The content of the header
///
/// # Returns
/// A boolean indicating if the header matches the ROM format.
fn format_matches(header: &[u8]) -> bool;
/// Load a ROM file.
///
/// # Arguments
/// * `header` - The header of the ROM
/// * `file_data` - The data from the file
///
/// # Returns
/// The ROM that was loaded
fn load(header: &[u8], file_data: &[u8]) -> Result<Rom, RomReadError>;
}
impl Rom {
fn read(path: &str) -> Result<(), RomReadError> {
info!("ROM - Reading file from {}", path);
let content = match fs::read(path) {
Ok(content) => content,
Err(error) => return Err(RomReadError::Io(error)),
};
if content.len() < 16 {
return Err(RomReadError::MissingHeader)
}
let header = &content[0..16];
if !INesHeader::format_matches(header) {
return Err(RomReadError::FormatNotSupported)
}
info!("ROM - Matched format iNes 2.0");
let rom = INesHeader::load(header, &content[16..])?;
debug!("ROM - PRG ROM: {} bytes, CHR ROM: {} bytes", rom.prg_rom_size, rom.chr_rom_size);
info!("ROM - Loading successful");
Ok(())
}
}
enum RomReadError {
FormatNotSupported,
Io(std::io::Error),
InvalidHeader(String),
MissingHeader,
RomLengthMismatch(usize, usize),
}
impl Display for RomReadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match *self {
RomReadError::FormatNotSupported => write!(f, "ROM didn't match any supported format"),
RomReadError::Io(ref err) => write!(f, "IO error: {}", err),
RomReadError::InvalidHeader(ref message) => write!(f, "Invalid ROM header: {}", message),
RomReadError::MissingHeader => write!(f, "File was too short to contain a header"),
RomReadError::RomLengthMismatch(required_length, actual_length) =>
write!(f, "ROM didn't contain enough data according to the header; required {} bytes but contained {} bytes", required_length, actual_length),
}
}
}