// The cartridge: iNES parsing and memory mapping.
//
// A cartridge is two ROM chips and a wiring choice. The iNES file format puts a
// 16-byte header in front of the chips, and the header's mapper number selects
// the wiring; everything a game does to bank-switch is a write to an address
// that the mapper intercepts instead of letting it reach RAM.
//
// The ROMs are kept as they appear in the file rather than being copied into
// the address space, because a mapper switches which slice answers an address
// and copying would mean swapping bytes on every bank change.
///|
/// The four-byte magic that starts an iNES image: "NES" followed by $1A.
///
/// `$1A` is a DOS end-of-file marker, chosen by the format's author so that
/// `type file.nes` on a DOS prompt would stop there rather than printing the
/// ROM as text.
const INES_MAGIC_0 : UInt32 = 0x4E
///|
const INES_MAGIC_1 : UInt32 = 0x45
///|
const INES_MAGIC_2 : UInt32 = 0x53
///|
const INES_MAGIC_3 : UInt32 = 0x1A
///|
/// Which set of nametables the PPU sees.
///
/// The console has 2 KiB of video RAM for nametables, but the PPU can address
/// four 1 KiB tables; the cartridge decides whether the two physical ones appear
/// twice, or are replaced by extra RAM on the cartridge.
pub(all) enum Mirroring {
/// Two tables, each visible twice, arranged to look like a screen taller than
/// it is wide.
Horizontal
/// Two tables, each visible twice, arranged side by side.
Vertical
/// All four virtual tables map the same 1 KiB; used by single-screen games.
SingleLow
SingleHigh
/// The cartridge supplies its own video RAM and the header's value is
/// ignored.
FourScreen
}
///|
/// A parsed cartridge.
pub(all) struct Cart {
/// Which physical nametables answer the PPU's four virtual ones.
mirroring : Mirroring
/// True when the cartridge has battery-backed save RAM.
has_battery : Bool
/// Program ROM as stored in the file, without the header.
prg : Bytes
/// Character memory: ROM from the file, or zeroed RAM when the header asks for
/// writable character memory. Held as a mutable array because a CHR-RAM
/// cartridge writes pattern data at run time and copying the chip on every
/// write would dominate a frame.
chr : FixedArray[Byte]
/// True when the header asked for writable character RAM.
chr_ram : Bool
/// Mapper number from the header.
mut mapper : MapperKind
/// 1 or 2, meaning 16 KiB or 32 KiB program banks.
prg_banks : Int64
/// Number of 8 KiB character banks; zero for a CHR-RAM cartridge.
chr_banks : Int64
/// Which 16 KiB program bank answers $8000. The upper window at $C000 is
/// always the last bank, which is what lets a bank-switched game keep its
/// vectors at a fixed address.
mut prg_bank : Int64
/// Which 16 KiB program bank answers $C000; only 32 KiB boards switch it.
mut prg_bank_high : Int64
/// Which 8 KiB character bank answers the PPU; only CHR-ROM boards switch it.
mut chr_bank : Int64
/// MMC1's five-write shift register: the value being assembled, and how many
/// bits have arrived.
mut shift : UInt32
mut shift_count : UInt32
/// MMC1's control register, which holds the mirroring bits among others.
mut control : UInt32
/// Set once the CPU has written to MMC1's control register, after which the
/// header's mirroring value no longer applies.
mut mirroring_from_mapper : Bool
/// MMC3's eight bank registers, in the order they are written: two 2 KiB
/// character banks, four 1 KiB character banks, then two 8 KiB program banks.
///
/// They are kept as written rather than decoded into a single bank number
/// because which window each one drives depends on a separate mode bit, and a
/// program can flip that bit without rewriting them.
mut bank_registers : FixedArray[Int64]
/// MMC3's bank-select register: which of the eight the next write targets.
mut bank_select : UInt32
/// MMC3's program and character bank arrangement bit, which swaps the two
/// halves of each address space.
mut prg_mode : Bool
mut chr_mode : Bool
/// MMC3's scanline counter, loaded with the value a program writes and
/// decremented on each A12 rising edge.
mut irq_counter : UInt32
mut irq_latch : UInt32
/// True once the counter has reached zero and the interrupt is asserted.
mut irq_line : Bool
/// True when the counter reloads itself on reaching zero, which is how a game
/// gets an interrupt on every scanline band rather than once.
mut irq_enabled : Bool
}
///|
/// The mappers this build implements.
///
/// The set is chosen by how much of the licensed library each one unlocks per
/// line of code rather than by number: NROM is the boards with no banking at
/// all, UxROM and CNROM are single-register boards, and MMC1 is a shift register
/// that most of the early library uses.
pub(all) enum MapperKind {
/// No banking. $8000 and $C000 see the same chip on a 16 KiB board.
Nrom
/// UxROM (mapper 2): one register selects the 16 KiB bank at $8000 and the
/// last bank is fixed at $C000.
UxRom
/// CNROM (mapper 3): one register selects the 8 KiB character bank, and the
/// program ROM is fixed.
Cnrom
/// MMC1 (mapper 1): a serial shift register loads five registers, so a program
/// has to write one bit at a time across five stores.
Mmc1
/// MMC3 (mapper 4): eight bank registers selected by index, and a scanline
/// counter driven by the PPU's A12 line that can interrupt the CPU mid-frame.
Mmc3
/// A mapper number this build has no wiring for; loading succeeds so the
/// header can still be inspected, but running raises when the CPU first
/// touches cartridge space.
Unsupported(UInt32)
}
///|
/// Threshold beyond which a declared ROM size is rejected before allocating.
///
/// The largest licensed NES game is smaller than 4 MiB; the limit exists so that
/// a header claiming 255 banks of 16 KiB in a 40 KiB file is refused instead of
/// trusted.
const MAX_PRG_BYTES : Int64 = 4L * 1024L * 1024L
///|
const MAX_CHR_BYTES : Int64 = 4L * 1024L * 1024L
///|
/// Parses an iNES image.
///
/// The 16-byte header is validated in two stages: the magic and the declared
/// sizes must be consistent with the file, and then the mapper must be one this
/// build can wire. A bad header is an error; an unknown mapper is not, because
/// the cartridge's identity is still worth reporting.
pub fn Cart::parse(bytes : Bytes) -> Cart raise NesError {
let length = bytes.length().to_int64()
if length < 16L {
raise UnexpectedEnd(offset=0L, needed=16L, available=length)
}
if bytes.at(0).to_int().reinterpret_as_uint() != INES_MAGIC_0 ||
bytes.at(1).to_int().reinterpret_as_uint() != INES_MAGIC_1 ||
bytes.at(2).to_int().reinterpret_as_uint() != INES_MAGIC_2 ||
bytes.at(3).to_int().reinterpret_as_uint() != INES_MAGIC_3 {
raise NotAnINes
}
let flags6 = flag_byte(bytes, 6)
let flags7 = flag_byte(bytes, 7)
let prg_banks = prg_banks_in_16k_units(bytes)
let chr_banks = chr_banks_in_8k_units(bytes)
let declared_prg = prg_banks.to_int64() * 16384L
let declared_chr = chr_banks.to_int64() * 8192L
if declared_prg > MAX_PRG_BYTES {
raise Truncated(
kind="program ROM",
declared=declared_prg,
present=MAX_PRG_BYTES,
)
}
if declared_chr > MAX_CHR_BYTES {
raise Truncated(
kind="character ROM",
declared=declared_chr,
present=MAX_CHR_BYTES,
)
}
let body = length - 16L
// A trainer is 512 bytes between the header and the chips. It is preserved in
// the file but never executed, so it only shifts where the chips start.
let has_trainer = (flags6 & 0x04) != 0
let trainer_bytes = if has_trainer { 512L } else { 0L }
let available = body - trainer_bytes
if declared_prg > available {
raise Truncated(
kind="program ROM",
declared=declared_prg,
present=available,
)
}
if declared_chr > available - declared_prg {
raise Truncated(
kind="character ROM",
declared=declared_chr,
present=available - declared_prg,
)
}
let prg_at = 16L + trainer_bytes
let prg = slice_bytes(bytes, prg_at, declared_prg)
let chr_bytes = slice_bytes(bytes, prg_at + declared_prg, declared_chr)
// A CHR-RAM cartridge declares zero banks; it still needs 8 KiB of backing
// store, because the PPU addresses it as if a chip were present.
let chr_size = if chr_banks == 0 { 8192 } else { chr_bytes.length() }
let chr : FixedArray[Byte] = FixedArray::make(chr_size, b'\x00')
for i in 0..> 4) & 0x0F)).reinterpret_as_int()
let chr_ram = chr_banks == 0
{
mirroring: mirroring_of(flags6),
has_battery: (flags6 & 0x02) != 0,
prg,
chr,
chr_ram,
mapper: mapper_of(mapper_number),
prg_banks: prg_banks.to_int64(),
chr_banks: chr_banks.to_int64(),
// UxROM powers up with the second-to-last bank selected, which is $FF on a
// board with fewer than 256 banks; the mask in the register write makes that
// the highest bank anyway. Resetting to the last bank instead would hide a
// program's very first bank switch behind a correct-looking screen.
prg_bank: 0L,
prg_bank_high: prg_banks.to_int64() - 2L,
chr_bank: 0L,
shift: 0U,
shift_count: 0U,
// $0C is the control register's power-on value: mode 3, so $C000 is fixed
// to the last bank and reset code can bank-switch without losing itself.
control: 0x0CU,
mirroring_from_mapper: false,
bank_registers: FixedArray::make(8, 0L),
bank_select: 0U,
prg_mode: false,
chr_mode: false,
irq_counter: 0U,
irq_latch: 0U,
irq_line: false,
irq_enabled: false,
}
}
///|
/// Reads one header byte as a `UInt32`.
fn flag_byte(bytes : Bytes, at : Int32) -> UInt32 {
bytes.at(at).to_int().reinterpret_as_uint() & 0xFF
}
///|
/// The 16 KiB program-bank count the header declares.
///
/// The field is one byte, but byte 9's low nibble became the high nibble of a
/// twelve-bit count in the later revision of the format; honouring it here means
/// a large homebrew ROM reports its real size rather than sixteen banks.
fn prg_banks_in_16k_units(bytes : Bytes) -> Int32 {
let low = flag_byte(bytes, 4).reinterpret_as_int()
let high = flag_byte(bytes, 9).reinterpret_as_int() & 0x0F
if high == 0x0F {
// $F in the high nibble is the old marker for the exponent-multiplier form,
// which this build does not decode; fall back to the plain count.
low
} else {
low | (high << 8)
}
}
///|
/// The 8 KiB character-bank count the header declares.
fn chr_banks_in_8k_units(bytes : Bytes) -> Int32 {
let low = flag_byte(bytes, 5).reinterpret_as_int()
let high = flag_byte(bytes, 9).reinterpret_as_int() & 0xF0
if high == 0xF0 {
low
} else {
low | high
}
}
///|
/// Decodes the mirroring and four-screen bits.
fn mirroring_of(flags6 : UInt32) -> Mirroring {
if (flags6 & 0x08) != 0 {
FourScreen
} else if (flags6 & 0x01) != 0 {
Vertical
} else {
Horizontal
}
}
///|
/// Copies a range out of the image.
fn slice_bytes(bytes : Bytes, start : Int64, count : Int64) -> Bytes {
let out = Buffer::Buffer(size_hint=count.to_int())
for i in 0.. UInt32 {
let index = offset % self.prg.length().to_int64()
self.prg.at(index.to_int()).to_int().reinterpret_as_uint() & 0xFF
}
///|
/// Reads a character-ROM byte at a cartridge-relative offset.
///
/// A CHR-RAM cartridge hands this function an empty array; the PPU owns the RAM
/// and answers pattern fetches itself before reaching here.
pub fn Cart::chr_byte(self : Cart, offset : Int64) -> UInt32 {
let index = offset % self.chr.length().to_int64()
self.chr[index.to_int()].to_int().reinterpret_as_uint() & 0xFF
}
///|
/// Writes a character-ROM byte, which only a CHR-RAM cartridge accepts.
///
/// Returns true when the write landed, so the caller knows whether the PPU
/// should have handled it instead.
pub fn Cart::write_chr(self : Cart, offset : Int64, value : UInt32) -> Bool {
if !self.chr_ram {
return false
}
let index = (offset % self.chr.length().to_int64()).to_int()
self.chr[index] = (value & 0xFF).to_byte()
true
}
///|
/// A short name for the mapper, used in reports.
pub fn MapperKind::name(self : MapperKind) -> String {
match self {
Nrom => "NROM"
UxRom => "UxROM"
Cnrom => "CNROM"
Mmc1 => "MMC1"
Mmc3 => "MMC3"
Unsupported(number) => "mapper \{number} (unsupported)"
}
}
///|
/// A short name for the mirroring, used in reports.
pub fn Mirroring::name(self : Mirroring) -> String {
match self {
Horizontal => "horizontal"
Vertical => "vertical"
SingleLow => "single-screen low"
SingleHigh => "single-screen high"
FourScreen => "four-screen"
}
}
///|
///|
///|
///