use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::space::{AddressSpace, Mapping, MappingId, RamStore, Region, RegionRef, RomWrite};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::machine::realize::Instance;
use super::ines::{Cartridge, Chr};
const WORK_RAM_BASE: u64 = 0x6000;
const WORK_RAM_WINDOW: u64 = 0x2000;
const PRG_BASE: u64 = 0x8000;
const PRG_WINDOW: u64 = 0x8000;
const CHR_BASE: u64 = 0x0000;
const CHR_WINDOW: u64 = 0x2000;
const NAMETABLE_BASE: u64 = 0x2000;
const NAMETABLE_WINDOW: u64 = 0x1000;
const NAMETABLE_SIZE: u64 = 0x0400;
const NAMETABLE_MIRROR_BASE: u64 = 0x3000;
const NAMETABLE_MIRROR_WINDOW: u64 = 0x0f00;
const CIRAM_LEN: u64 = 0x0800;
const CPU_SPACE_LEN: u64 = 0x1_0000;
const PPU_SPACE_LEN: u64 = 0x4000;
const STATE_VERSION: u32 = 1;
const CLASS_NAME: &str = "nes.nrom";
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct CartMappings {
pub cpu: Vec<MappingId>,
pub ppu: Vec<MappingId>,
}
#[derive(Debug)]
pub struct Nrom {
cart: Cartridge,
prg_window: RegionRef,
chr_window: RegionRef,
work_ram_window: Option<RegionRef>,
vram: Option<Arc<RamStore>>,
vram_region: Option<RegionRef>,
}
impl Nrom {
pub fn new(cart: Cartridge) -> Result<Nrom> {
if cart.mapper() != 0 {
return Err(config(format!(
"cartridge names mapper {}, which is not NROM (0)",
cart.mapper()
)));
}
let prg = cart.prg_rom();
check_window("PRG ROM", prg.len(), PRG_WINDOW)?;
let prg_rom = Arc::new(Region::rom(
"nes.nrom.prg-rom",
prg.clone(),
RomWrite::Ignore,
));
let prg_window = Arc::new(Region::mirror("nes.nrom.prg", prg_rom, PRG_WINDOW)?);
let chr_window = match cart.chr() {
Chr::Rom(rom) => {
check_window("CHR ROM", rom.len(), CHR_WINDOW)?;
let region = Arc::new(Region::rom(
"nes.nrom.chr-rom",
rom.clone(),
RomWrite::Ignore,
));
Arc::new(Region::mirror("nes.nrom.chr", region, CHR_WINDOW)?)
}
Chr::Ram(ram) => {
check_window("CHR RAM", ram.len(), CHR_WINDOW)?;
let region = Arc::new(Region::ram("nes.nrom.chr-ram", ram.clone()));
Arc::new(Region::mirror("nes.nrom.chr", region, CHR_WINDOW)?)
}
};
let work_ram_window = match cart.work_ram() {
None => None,
Some(ram) => {
check_window("work RAM", ram.len(), WORK_RAM_WINDOW)?;
let region = Arc::new(Region::ram("nes.nrom.work-ram", ram.clone()));
Some(Arc::new(Region::mirror(
"nes.nrom.work",
region,
WORK_RAM_WINDOW,
)?))
}
};
let vram = if cart.mirroring().needs_cartridge_vram() {
Some(Arc::new(RamStore::new(CIRAM_LEN)))
} else {
None
};
let vram_region = vram
.as_ref()
.map(|v| Arc::new(Region::ram("nes.nrom.vram", Arc::clone(v))) as RegionRef);
Ok(Nrom {
cart,
prg_window,
chr_window,
work_ram_window,
vram,
vram_region,
})
}
pub fn from_image(bytes: &[u8]) -> Result<Nrom> {
Nrom::new(Cartridge::from_ines(bytes)?)
}
pub fn from_props(props: &Props) -> Result<Nrom> {
let mut r = props.reader();
let image = r.require_media("rom")?.to_bytes();
r.finish()?;
Nrom::from_image(&image)
}
#[must_use]
pub const fn cartridge(&self) -> &Cartridge {
&self.cart
}
#[must_use]
pub const fn cartridge_vram(&self) -> Option<&Arc<RamStore>> {
self.vram.as_ref()
}
pub fn install(
&self,
cpu: &AddressSpace,
ppu: &AddressSpace,
ciram: &Arc<RamStore>,
) -> Result<CartMappings> {
if cpu.size() < CPU_SPACE_LEN {
return Err(config(format!(
"CPU space `{}` is {:#x} bytes; a 6502 bus is {CPU_SPACE_LEN:#x}",
cpu.name(),
cpu.size()
)));
}
if ppu.size() < PPU_SPACE_LEN {
return Err(config(format!(
"PPU space `{}` is {:#x} bytes; the PPU decodes {PPU_SPACE_LEN:#x}",
ppu.name(),
ppu.size()
)));
}
if ciram.len() < CIRAM_LEN {
return Err(config(format!(
"CIRAM is {:#x} bytes; the console has {CIRAM_LEN:#x}",
ciram.len()
)));
}
let nametables = Arc::new(self.nametables(ciram)?);
let nametable_mirror = Arc::new(Region::alias(
"nes.nrom.nametables-mirror",
nametables.clone(),
0,
NAMETABLE_MIRROR_WINDOW,
)?);
let mut mappings = CartMappings::default();
{
let mut topo = cpu.topology();
if let Some(work) = &self.work_ram_window {
mappings.cpu.push(topo.map(work.clone(), WORK_RAM_BASE)?);
}
mappings
.cpu
.push(topo.map(self.prg_window.clone(), PRG_BASE)?);
}
{
let mut topo = ppu.topology();
mappings
.ppu
.push(topo.map(self.chr_window.clone(), CHR_BASE)?);
mappings.ppu.push(topo.map(nametables, NAMETABLE_BASE)?);
mappings
.ppu
.push(topo.map(nametable_mirror, NAMETABLE_MIRROR_BASE)?);
}
Ok(mappings)
}
pub fn uninstall(
&self,
cpu: &AddressSpace,
ppu: &AddressSpace,
mappings: &CartMappings,
) -> Result<()> {
{
let mut topo = cpu.topology();
for id in &mappings.cpu {
topo.unmap(*id)?;
}
}
{
let mut topo = ppu.topology();
for id in &mappings.ppu {
topo.unmap(*id)?;
}
}
Ok(())
}
fn nametables(&self, ciram: &Arc<RamStore>) -> Result<Region> {
let console = Arc::new(Region::ram("nes.ciram", ciram.clone()));
let cart_vram = self
.vram
.as_ref()
.map(|v| Arc::new(Region::ram("nes.nrom.vram", v.clone())));
let mut children = Vec::with_capacity(4);
for (slot, bank) in self.cart.mirroring().banks().into_iter().enumerate() {
let (target, index) = if bank < 2 {
(&console, u64::from(bank))
} else {
let vram = cart_vram.as_ref().ok_or_else(|| {
config(String::from(
"four-screen mirroring needs cartridge VRAM, which this board has none of",
))
})?;
(vram, u64::from(bank) - 2)
};
let name = match slot {
0 => "nes.nrom.nt0",
1 => "nes.nrom.nt1",
2 => "nes.nrom.nt2",
_ => "nes.nrom.nt3",
};
let window =
Region::alias(name, target.clone(), index * NAMETABLE_SIZE, NAMETABLE_SIZE)?;
children.push(Mapping::new(window, slot as u64 * NAMETABLE_SIZE));
}
Ok(Region::container(
"nes.nrom.nametables",
NAMETABLE_WINDOW,
children,
))
}
fn mutable_stores(&self) -> [Option<&Arc<RamStore>>; 3] {
[
self.cart.work_ram(),
self.cart.chr().as_ram(),
self.vram.as_ref(),
]
}
}
fn check_window(what: &str, len: u64, window: u64) -> Result<()> {
if len == 0 {
return Err(config(format!(
"NROM needs some {what}; the cartridge has none"
)));
}
if len > window {
return Err(config(format!(
"{what} is {len:#x} bytes, more than NROM's {window:#x} window can address"
)));
}
if !len.is_power_of_two() {
return Err(config(format!(
"{what} is {len:#x} bytes, which no arrangement of address lines produces"
)));
}
Ok(())
}
fn config(message: String) -> Error {
Error::Config {
at: String::from(CLASS_NAME),
message,
}
}
fn read_store(store: &RamStore) -> Result<Vec<u8>> {
let len = usize::try_from(store.len())
.map_err(|_| Error::State(String::from("RAM larger than the host address space")))?;
let mut buf = alloc::vec![0u8; len];
store
.read_at(0, &mut buf)
.map_err(|e| Error::State(format!("cannot read cartridge RAM: {e}")))?;
Ok(buf)
}
pub static NROM_CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "NES NROM cartridge (iNES mapper 0): fixed PRG and CHR windows, no banking",
properties: &[PropertySpec {
name: "rom",
kind: ValueKind::Media,
required: true,
summary: "the iNES image, as the name of a media slot (`rom = \"cart\"`)",
}],
construct: |props| Ok(Box::new(Nrom::from_props(props)?)),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&NROM_CLASS)
}
impl Device for Nrom {
fn class(&self) -> &'static DeviceClass {
&NROM_CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
"prg" => Some(Arc::clone(&self.prg_window)),
"chr" => Some(Arc::clone(&self.chr_window)),
"work" => self.work_ram_window.clone(),
"vram" => self.vram_region.clone(),
_ => None,
}
}
fn reset(&self, kind: ResetKind) {
if kind != ResetKind::Cold {
return;
}
if let Some(chr) = self.cart.chr().as_ram() {
let _ = chr.fill(0, chr.len(), 0);
}
if let Some(vram) = &self.vram {
let _ = vram.fill(0, vram.len(), 0);
}
if let Some(work) = self.cart.work_ram() {
if !self.cart.battery() {
let _ = work.fill(0, work.len(), 0);
}
}
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
for store in self.mutable_stores() {
match store {
Some(s) => {
w.write_bool(true)?;
w.write_bytes(&read_store(s)?)?;
}
None => w.write_bool(false)?,
}
}
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
for (i, store) in self.mutable_stores().into_iter().enumerate() {
let name = ["work RAM", "CHR RAM", "four-screen VRAM"][i];
let present = r.read_bool()?;
match (present, store) {
(false, None) => {}
(true, Some(s)) => {
let bytes = r.read_bytes()?;
if bytes.len() as u64 != s.len() {
return Err(Error::State(format!(
"snapshot has {} byte(s) of {name}, but this cartridge has {}",
bytes.len(),
s.len()
)));
}
s.write_at(0, bytes)
.map_err(|e| Error::State(format!("cannot restore {name}: {e}")))?;
}
(true, None) => {
return Err(Error::State(format!(
"snapshot has {name}, but this cartridge has none"
)));
}
(false, Some(_)) => {
return Err(Error::State(format!(
"snapshot has no {name}, but this cartridge has some"
)));
}
}
}
Ok(())
}
}
impl Instance for Nrom {}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(Nrom::from_props(props)?)))
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PropSchema};
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("rom", ValueKind::Media).required())
.region("prg")
.region("chr")
.region("work")
.region("vram")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::props::Media;
use crate::core::space::MemAttrs;
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
use crate::core::value::Width;
use crate::dev::cart::ines::Mirroring;
use alloc::vec;
fn image(prg_units: u8, chr_units: u8, flags6: u8) -> Vec<u8> {
let mut v = vec![0u8; 16];
v[..4].copy_from_slice(b"NES\x1a");
v[4] = prg_units;
v[5] = chr_units;
v[6] = flags6;
let prg_len = usize::from(prg_units) * 16384;
for i in 0..prg_len {
v.push((i >> 8) as u8 ^ (i as u8));
}
let chr_len = usize::from(chr_units) * 8192;
for i in 0..chr_len {
v.push(!((i >> 8) as u8 ^ (i as u8)));
}
v
}
fn board(prg_units: u8, chr_units: u8, flags6: u8) -> Nrom {
let cart = Cartridge::from_ines(&image(prg_units, chr_units, flags6)).expect("valid image");
Nrom::new(cart).expect("an NROM board")
}
struct Bus {
cpu: AddressSpace,
ppu: AddressSpace,
ciram: Arc<RamStore>,
}
fn bus() -> Bus {
Bus {
cpu: AddressSpace::new("cpu", 16),
ppu: AddressSpace::new("ppu", 14),
ciram: Arc::new(RamStore::new(CIRAM_LEN)),
}
}
fn rd(space: &AddressSpace, addr: u64) -> u8 {
space
.read(addr, Width::U8, MemAttrs::DEFAULT)
.unwrap_or_else(|e| panic!("read {addr:#06x}: {e}")) as u8
}
fn wr(space: &AddressSpace, addr: u64, value: u8) {
space
.write(addr, Width::U8, u64::from(value), MemAttrs::DEFAULT)
.unwrap_or_else(|e| panic!("write {addr:#06x}: {e}"));
}
#[test]
fn sixteen_kib_of_prg_answers_in_both_banks() {
let nrom = board(1, 1, 0);
let b = bus();
nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
for offset in [0u64, 1, 0x1234, 0x3ffc, 0x3fff] {
let low = rd(&b.cpu, PRG_BASE + offset);
let high = rd(&b.cpu, PRG_BASE + 0x4000 + offset);
assert_eq!(low, high, "offset {offset:#06x} must mirror");
let want = ((offset >> 8) as u8) ^ (offset as u8);
assert_eq!(low, want, "offset {offset:#06x}");
}
assert_eq!(rd(&b.cpu, 0xfffc), rd(&b.cpu, 0xbffc));
let view = b.cpu.view();
let idx = view.locate(PRG_BASE).expect("mapped");
let entry = view.flat_view().entry(idx).expect("entry");
assert_eq!(entry.start(), PRG_BASE);
assert_eq!(entry.len(), PRG_WINDOW);
}
#[test]
fn thirty_two_kib_of_prg_is_contiguous() {
let nrom = board(2, 1, 0);
let b = bus();
nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
for offset in [0u64, 0x3fff, 0x4000, 0x7fff] {
let want = ((offset >> 8) as u8) ^ (offset as u8);
assert_eq!(rd(&b.cpu, PRG_BASE + offset), want, "offset {offset:#06x}");
}
assert_ne!(rd(&b.cpu, 0x8001), rd(&b.cpu, 0xc001));
}
#[test]
fn prg_rom_ignores_writes() {
let nrom = board(2, 1, 0);
let b = bus();
nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
let before = rd(&b.cpu, 0x8000);
wr(&b.cpu, 0x8000, before.wrapping_add(1));
assert_eq!(rd(&b.cpu, 0x8000), before, "a mask ROM swallows writes");
}
#[test]
fn chr_rom_is_readable_and_chr_ram_is_writable() {
let nrom = board(1, 1, 0);
let b = bus();
nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
for offset in [0u64, 0x1000, 0x1fff] {
let want = !(((offset >> 8) as u8) ^ (offset as u8));
assert_eq!(rd(&b.ppu, CHR_BASE + offset), want, "chr {offset:#06x}");
}
wr(&b.ppu, 0x0100, 0x99);
assert_ne!(rd(&b.ppu, 0x0100), 0x99, "CHR ROM is not writable");
let nrom = board(1, 0, 0);
let b = bus();
nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
assert!(nrom.cartridge().chr().is_ram());
wr(&b.ppu, 0x0100, 0x99);
assert_eq!(rd(&b.ppu, 0x0100), 0x99);
}
#[test]
fn work_ram_is_mapped_and_mirrored() {
let nrom = board(1, 1, 0);
let b = bus();
nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
wr(&b.cpu, 0x6000, 0x42);
assert_eq!(rd(&b.cpu, 0x6000), 0x42);
wr(&b.cpu, 0x7fff, 0x24);
assert_eq!(rd(&b.cpu, 0x7fff), 0x24);
assert_eq!(rd(&b.cpu, 0x6000), 0x42);
}
#[test]
fn a_board_with_no_work_ram_maps_none() {
let mut h = [0u8; 16];
h[..4].copy_from_slice(b"NES\x1a");
h[4] = 1;
h[7] = 0x08;
h[11] = 0x07; let mut img = h.to_vec();
img.extend(core::iter::repeat_n(0u8, 16384));
let cart = Cartridge::from_ines(&img).expect("valid image");
assert!(cart.work_ram().is_none());
let nrom = Nrom::new(cart).expect("board");
let b = bus();
let m = nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
assert_eq!(m.cpu.len(), 1, "only the PRG window");
assert!(b.cpu.locate(0x6000).is_none(), "$6000 is open bus");
}
fn nametable_pattern(nrom: &Nrom) -> [[u8; 4]; 4] {
let b = bus();
nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
let mut out = [[0u8; 4]; 4];
for (written, row) in out.iter_mut().enumerate() {
for slot in 0..4u64 {
wr(&b.ppu, NAMETABLE_BASE + slot * NAMETABLE_SIZE, 0);
}
wr(
&b.ppu,
NAMETABLE_BASE + written as u64 * NAMETABLE_SIZE,
0x80 | written as u8,
);
for (slot, cell) in row.iter_mut().enumerate() {
*cell = rd(&b.ppu, NAMETABLE_BASE + slot as u64 * NAMETABLE_SIZE);
}
}
out
}
#[test]
fn horizontal_mirroring_pairs_the_first_two_nametables() {
let pattern = nametable_pattern(&board(1, 1, 0x00));
assert_eq!(pattern[0], [0x80, 0x80, 0, 0]);
assert_eq!(pattern[2], [0, 0, 0x82, 0x82]);
}
#[test]
fn vertical_mirroring_pairs_alternate_nametables() {
let pattern = nametable_pattern(&board(1, 1, 0x01));
assert_eq!(pattern[0], [0x80, 0, 0x80, 0]);
assert_eq!(pattern[1], [0, 0x81, 0, 0x81]);
}
#[test]
fn four_screen_mirroring_keeps_all_four_distinct() {
let nrom = board(1, 1, 0x08);
assert_eq!(nrom.cartridge().mirroring(), Mirroring::FourScreen);
assert!(nrom.cartridge_vram().is_some());
let pattern = nametable_pattern(&nrom);
for (i, row) in pattern.iter().enumerate() {
let mut want = [0u8; 4];
want[i] = 0x80 | i as u8;
assert_eq!(*row, want, "slot {i}");
}
}
#[test]
fn the_two_ciram_banks_are_distinct_storage() {
assert_eq!(Mirroring::SingleScreenLower.banks(), [0; 4]);
assert_eq!(Mirroring::SingleScreenUpper.banks(), [1; 4]);
let nrom = board(1, 1, 0x00);
let b = bus();
nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
wr(&b.ppu, 0x2000, 0x11);
wr(&b.ppu, 0x2800, 0x22);
assert_eq!(b.ciram.read_u8(0).expect("in range"), 0x11);
assert_eq!(b.ciram.read_u8(0x400).expect("in range"), 0x22);
}
#[test]
fn the_nametables_appear_again_at_3000() {
let nrom = board(1, 1, 0x01);
let b = bus();
nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
wr(&b.ppu, 0x2000, 0x5a);
assert_eq!(rd(&b.ppu, 0x3000), 0x5a);
wr(&b.ppu, 0x3eff, 0xa5);
assert_eq!(rd(&b.ppu, 0x2eff), 0xa5);
assert!(b.ppu.locate(0x3f00).is_none());
}
#[test]
fn uninstall_puts_the_spaces_back() {
let nrom = board(1, 1, 0);
let b = bus();
let m = nrom.install(&b.cpu, &b.ppu, &b.ciram).expect("installs");
assert!(b.cpu.locate(0x8000).is_some());
nrom.uninstall(&b.cpu, &b.ppu, &m).expect("unmaps");
assert!(b.cpu.locate(0x8000).is_none());
assert!(b.ppu.locate(0x0000).is_none());
assert!(b.ppu.locate(0x2000).is_none());
}
#[test]
fn a_non_nrom_cartridge_is_rejected() {
let mut img = image(1, 1, 0);
img[6] |= 0x10; let cart = Cartridge::from_ines(&img).expect("valid image");
let err = Nrom::new(cart).expect_err("not NROM");
assert!(alloc::format!("{err}").contains("mapper 1"), "{err}");
}
#[test]
fn more_prg_than_the_window_can_address_is_rejected() {
let cart = Cartridge::from_ines(&image(4, 1, 0)).expect("valid image");
let err = Nrom::new(cart).expect_err("64 KiB does not fit");
assert!(alloc::format!("{err}").contains("PRG ROM"), "{err}");
}
#[test]
fn a_non_power_of_two_rom_is_rejected() {
let mut h = [0u8; 16];
h[..4].copy_from_slice(b"NES\x1a");
h[7] = 0x08;
h[4] = (10 << 2) | 1; h[9] = 0x0f;
h[11] = 0x07;
let mut img = h.to_vec();
img.extend(core::iter::repeat_n(0u8, 3072));
let cart = Cartridge::from_ines(&img).expect("valid image");
let err = Nrom::new(cart).expect_err("3 KiB is not a power of two");
assert!(alloc::format!("{err}").contains("address lines"), "{err}");
}
#[test]
fn a_space_that_is_not_a_nes_bus_is_rejected() {
let nrom = board(1, 1, 0);
let cpu = AddressSpace::new("cpu", 15);
let ppu = AddressSpace::new("ppu", 14);
let ciram = Arc::new(RamStore::new(CIRAM_LEN));
assert!(nrom.install(&cpu, &ppu, &ciram).is_err());
let cpu = AddressSpace::new("cpu", 16);
let ppu = AddressSpace::new("ppu", 13);
assert!(nrom.install(&cpu, &ppu, &ciram).is_err());
let ppu = AddressSpace::new("ppu", 14);
let small = Arc::new(RamStore::new(1024));
assert!(nrom.install(&cpu, &ppu, &small).is_err());
}
#[test]
fn construction_needs_a_bound_rom() {
let err = (NROM_CLASS.construct)(&Props::new())
.expect_err("needs an image")
.to_string();
assert!(err.contains("rom") && err.contains("media"), "{err}");
let err = (NROM_CLASS.construct)(&Props::new().with("rom", "cart"))
.expect_err("nothing bound")
.to_string();
assert!(err.contains("cart"), "{err}");
}
#[test]
fn a_bound_image_constructs_the_board() {
let bytes: Arc<[u8]> = image(2, 1, 0).into();
let props = Props::new().with("rom", Media::new("cart", bytes));
let device = (NROM_CLASS.construct)(&props).expect("a real image");
assert_eq!(device.class().name, CLASS_NAME);
assert_eq!(device.region("prg").expect("prg").len(), PRG_WINDOW);
assert_eq!(device.region("chr").expect("chr").len(), CHR_WINDOW);
assert!(device.region("").is_none(), "no single aperture");
assert!(device.region("nonesuch").is_none());
}
#[test]
fn a_truncated_image_is_refused_by_name() {
let bytes: Arc<[u8]> = alloc::vec![0u8; 8].into();
let props = Props::new().with("rom", Media::new("cart", bytes));
assert!((NROM_CLASS.construct)(&props).is_err(), "eight bytes");
}
#[test]
fn the_class_registers_once() {
let mut reg = crate::core::Registry::new();
register(&mut reg).expect("first registration");
assert!(reg.get(CLASS_NAME).is_some());
assert!(register(&mut reg).is_err(), "twice is a feature collision");
}
#[test]
fn a_cold_reset_clears_volatile_ram_but_not_a_battery() {
let nrom = board(1, 0, 0x02); let chr = nrom.cartridge().chr().as_ram().expect("chr ram").clone();
let work = nrom.cartridge().work_ram().expect("work ram").clone();
chr.write_u8(0, 0xaa).expect("in range");
work.write_u8(0, 0xbb).expect("in range");
nrom.reset(ResetKind::Warm);
assert_eq!(
chr.read_u8(0).expect("in range"),
0xaa,
"a reset line is not power"
);
nrom.reset(ResetKind::Cold);
assert_eq!(chr.read_u8(0).expect("in range"), 0x00);
assert_eq!(
work.read_u8(0).expect("in range"),
0xbb,
"battery-backed RAM survives a power cycle"
);
let nrom = board(1, 0, 0x00);
let work = nrom.cartridge().work_ram().expect("work ram").clone();
work.write_u8(0, 0xbb).expect("in range");
nrom.reset(ResetKind::Cold);
assert_eq!(work.read_u8(0).expect("in range"), 0x00);
}
fn snapshot(nrom: &Nrom) -> Vec<u8> {
let mut shape = MachineShape::new();
shape.add_device("cart", CLASS_NAME).expect("unique path");
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer
.chunk("cart", CLASS_NAME, STATE_VERSION)
.expect("one chunk");
nrom.save(&mut chunk).expect("saves");
}
writer.to_vec().expect("encodes")
}
fn restore(nrom: &Nrom, bytes: &[u8]) {
let reader = StateReader::new(bytes).expect("decodes");
let chunk = reader
.load("cart", CLASS_NAME, STATE_VERSION, &Migrations::new())
.expect("finds the chunk");
let mut r = chunk.reader();
nrom.load(&mut r).expect("loads");
}
#[test]
fn state_round_trips_to_an_identical_hash() {
let nrom = board(1, 0, 0x08 | 0x01);
let chr = nrom.cartridge().chr().as_ram().expect("chr ram").clone();
let work = nrom.cartridge().work_ram().expect("work ram").clone();
let vram = nrom.cartridge_vram().expect("four-screen vram").clone();
for (i, store) in [&chr, &work, &vram].into_iter().enumerate() {
for off in 0..64u64 {
store
.write_u8(off, (off as u8).wrapping_mul(7).wrapping_add(i as u8))
.expect("in range");
}
}
let saved = snapshot(&nrom);
let other = board(1, 0, 0x08 | 0x01);
other
.cartridge()
.chr()
.as_ram()
.expect("chr ram")
.write_u8(0, 0xff)
.expect("in range");
assert_ne!(snapshot(&other), saved, "the boards start out different");
restore(&other, &saved);
assert_eq!(snapshot(&other), saved, "state hash must match after load");
let mut a = [0u8; 64];
let mut b = [0u8; 64];
chr.read_at(0, &mut a).expect("in range");
other
.cartridge()
.chr()
.as_ram()
.expect("chr ram")
.read_at(0, &mut b)
.expect("in range");
assert_eq!(a, b);
}
#[test]
fn a_snapshot_does_not_carry_rom() {
let nrom = board(2, 1, 0);
let mut shape = MachineShape::new();
shape.add_device("cart", CLASS_NAME).expect("unique path");
let mut writer = StateWriter::new(shape);
let mut chunk = writer
.chunk("cart", CLASS_NAME, STATE_VERSION)
.expect("one chunk");
nrom.save(&mut chunk).expect("saves");
assert_eq!(chunk.len(), 8192 + 8 + 3);
}
#[test]
fn a_snapshot_from_a_differently_shaped_board_is_refused() {
let with_vram = board(1, 0, 0x08);
let saved = snapshot(&with_vram);
let without = board(1, 0, 0x00);
let reader = StateReader::new(&saved).expect("decodes");
let chunk = reader
.load("cart", CLASS_NAME, STATE_VERSION, &Migrations::new())
.expect("finds the chunk");
let mut r = chunk.reader();
let err = without.load(&mut r).expect_err("shapes disagree");
assert!(alloc::format!("{err}").contains("VRAM"), "{err}");
}
#[test]
fn a_truncated_chunk_is_an_error_not_a_panic() {
let nrom = board(1, 0, 0);
let mut shape = MachineShape::new();
shape.add_device("cart", CLASS_NAME).expect("unique path");
let mut writer = StateWriter::new(shape);
{
let mut chunk = writer
.chunk("cart", CLASS_NAME, STATE_VERSION)
.expect("one chunk");
nrom.save(&mut chunk).expect("saves");
}
let bytes = writer.to_vec().expect("encodes");
for n in 0..bytes.len().min(256) {
let _ = StateReader::new(&bytes[..n]);
}
let reader = StateReader::new(&bytes).expect("decodes");
let (_, _, data) = reader.load_raw("cart").expect("raw chunk");
for n in 0..data.len().min(64) {
let mut r = ChunkReader::new(&data[..n]);
let _ = nrom.load(&mut r);
}
}
#[test]
fn the_device_trait_is_wired_up() {
let nrom = board(1, 1, 0);
assert_eq!(nrom.class().name, CLASS_NAME);
assert_eq!(nrom.class().version, STATE_VERSION);
let mut deferred = crate::core::device::Deferred::new();
let mut ctx = RealizeCtx::new("cart", crate::core::space::RequesterId(1), &mut deferred);
nrom.realize(&mut ctx).expect("realizes");
nrom.unrealize(&mut ctx).expect("unrealizes");
}
#[test]
fn a_board_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Nrom>();
assert_send_sync::<Cartridge>();
}
}