use alloc::string::String;
use alloc::vec::Vec;
use crate::core::error::Result;
use crate::core::registry::Registry;
use crate::machine::builtin;
use crate::machine::realize::Bindings;
use crate::machine::validate::ClassTable;
use crate::machine::{BuildOptions, Machine};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CatalogEntry {
pub name: &'static str,
pub summary: &'static str,
pub media: &'static [&'static str],
pub source: &'static str,
}
#[cfg(feature = "machine-nes")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-nes")))]
pub static NES_NTSC: CatalogEntry = CatalogEntry {
name: "nes-ntsc",
summary: "Nintendo Entertainment System / Famicom, NTSC (RP2C02 at 60 Hz)",
media: &["cart"],
source: include_str!("../../machines/nes-ntsc.machine"),
};
#[cfg(feature = "machine-nes")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-nes")))]
pub static NES_PAL: CatalogEntry = CatalogEntry {
name: "nes-pal",
summary: "Nintendo Entertainment System, PAL (RP2C07 at 50 Hz, 312 scanlines)",
media: &["cart"],
source: include_str!("../../machines/nes-pal.machine"),
};
#[cfg(feature = "machine-apple1")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-apple1")))]
pub static APPLE1: CatalogEntry = CatalogEntry {
name: "apple1",
summary: "Apple 1 (1976): 6502, 4K RAM, MC6821 keyboard and display",
media: &["rom"],
source: include_str!("../../machines/apple1.machine"),
};
#[cfg(feature = "machine-beneater")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-beneater")))]
pub static BENEATER_6502: CatalogEntry = CatalogEntry {
name: "beneater-6502",
summary: "Ben Eater's 6502 breadboard computer: 1 MHz, 16K RAM, 65C51 serial, 65C22",
media: &["rom"],
source: include_str!("../../machines/beneater-6502.machine"),
};
#[allow(unused_mut, clippy::vec_init_then_push)]
#[must_use]
pub fn machines() -> Vec<&'static CatalogEntry> {
let mut out: Vec<&'static CatalogEntry> = Vec::new();
#[cfg(feature = "machine-apple1")]
out.push(&APPLE1);
#[cfg(feature = "machine-beneater")]
out.push(&BENEATER_6502);
#[cfg(feature = "machine-nes")]
out.push(&NES_NTSC);
#[cfg(feature = "machine-nes")]
out.push(&NES_PAL);
out
}
#[must_use]
pub fn machine(name: &str) -> Option<&'static CatalogEntry> {
let stem = name.strip_suffix(".machine").unwrap_or(name);
machines().into_iter().find(|m| m.name == stem)
}
pub fn registry() -> Result<Registry> {
let mut reg = Registry::new();
builtin::register(&mut reg)?;
#[cfg(feature = "cpu-mos6502")]
crate::cpu::mos6502::register(&mut reg)?;
#[cfg(feature = "dev-nes-cart")]
crate::dev::cart::nrom::register(&mut reg)?;
#[cfg(feature = "dev-nes-ppu")]
crate::dev::ppu::register(&mut reg)?;
#[cfg(feature = "dev-nes-apu")]
crate::dev::apu::register(&mut reg)?;
#[cfg(feature = "dev-apple1")]
crate::dev::apple1::register(&mut reg)?;
#[cfg(feature = "dev-wdc")]
crate::dev::wdc::register(&mut reg)?;
Ok(reg)
}
pub fn bindings() -> Result<Bindings> {
let mut b = Bindings::new();
builtin::bind(&mut b)?;
#[cfg(feature = "cpu-mos6502")]
crate::cpu::mos6502::bind(&mut b)?;
#[cfg(feature = "dev-nes-cart")]
crate::dev::cart::nrom::bind(&mut b)?;
#[cfg(feature = "dev-nes-ppu")]
crate::dev::ppu::bind(&mut b)?;
#[cfg(feature = "dev-nes-apu")]
crate::dev::apu::bind(&mut b)?;
#[cfg(feature = "dev-apple1")]
crate::dev::apple1::bind(&mut b)?;
#[cfg(feature = "dev-wdc")]
crate::dev::wdc::bind(&mut b)?;
Ok(b)
}
#[must_use]
pub fn classes() -> ClassTable {
let mut table = ClassTable::new();
for schema in builtin::schemas() {
table.insert(schema);
}
#[cfg(feature = "cpu-mos6502")]
table.insert(crate::cpu::mos6502::schema());
#[cfg(feature = "dev-nes-cart")]
table.insert(crate::dev::cart::nrom::schema());
#[cfg(feature = "dev-nes-ppu")]
table.insert(crate::dev::ppu::schema());
#[cfg(feature = "dev-nes-apu")]
table.insert(crate::dev::apu::schema());
#[cfg(feature = "dev-apple1")]
for schema in crate::dev::apple1::schemas() {
table.insert(schema);
}
#[cfg(feature = "dev-wdc")]
for schema in crate::dev::wdc::schemas() {
table.insert(schema);
}
table
}
pub fn build_options() -> Result<BuildOptions> {
Ok(BuildOptions::new()
.with_classes(classes())
.with_bindings(bindings()?))
}
pub fn build_catalog(name: &str, media: &[(&str, &[u8])]) -> Result<Machine> {
let entry = machine(name).ok_or_else(|| unknown(name))?;
let mut options = build_options()?;
for (slot, bytes) in media {
options.realize.media.insert(*slot, *bytes);
}
crate::machine::build(entry.name, entry.source, ®istry()?, &options)
}
fn unknown(name: &str) -> crate::core::Error {
let mut message = String::from("no machine named `");
message.push_str(name);
message.push_str("` in this build; it has ");
let names: Vec<&str> = machines().into_iter().map(|m| m.name).collect();
if names.is_empty() {
message.push_str("none (enable a `machine-*` feature)");
} else {
for (i, n) in names.iter().enumerate() {
if i != 0 {
message.push_str(", ");
}
message.push('`');
message.push_str(n);
message.push('`');
}
}
crate::core::Error::Config {
at: String::from("catalog"),
message,
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn the_registry_and_the_bindings_agree() {
let reg = registry().expect("no class name collides");
let bound = bindings().expect("no binding collides");
for class in bound.classes() {
assert!(
reg.get(class).is_some(),
"`{class}` is bound but not registered"
);
}
assert!(reg.get("ram").is_some(), "the language's own class");
}
#[test]
fn every_shipped_machine_realizes() {
for entry in machines() {
let media: Vec<(&str, &[u8])> = entry
.media
.iter()
.map(|slot| (*slot, fixture(entry.name, slot)))
.collect();
match build_catalog(entry.name, &media) {
Ok(machine) => assert_eq!(machine.name(), entry.name),
Err(e) => panic!("{}: {e}", entry.name),
}
}
}
#[test]
fn an_unknown_machine_lists_what_there_is() {
let e = build_catalog("gameboy", &[])
.expect_err("no gameboy")
.to_string();
assert!(e.contains("gameboy"), "{e}");
}
#[cfg(feature = "cpu-mos6502")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CpuState {
a: u8,
x: u8,
y: u8,
s: u8,
p: u8,
pc: u16,
cycles: u64,
halted: bool,
reset_pending: bool,
faults: u64,
last_fault: u16,
}
#[cfg(feature = "cpu-mos6502")]
fn cpu_state(machine: &Machine, path: &str) -> CpuState {
use crate::core::state::{Migrations, Source, StateReader};
let class = &crate::cpu::mos6502::CLASS;
let bytes = machine.save().expect("a machine saves");
let reader = StateReader::new(&bytes).expect("well formed");
let chunk = reader
.load(path, class.name, class.version, &Migrations::new())
.expect("a chunk per device, keyed by instance path");
let mut r = chunk.reader();
let mut byte = || r.read_u8().expect("the chunk is not truncated");
let (a, x, y, s, p) = (byte(), byte(), byte(), byte(), byte());
let pc = r.read_u16().expect("pc");
let cycles = r.read_u64().expect("cycles");
let halted = r.read_bool().expect("halted");
let reset_pending = r.read_bool().expect("reset_pending");
let _pending_interrupt = r.read_u8().expect("pending");
let _open_bus = r.read_u8().expect("open bus");
let faults = r.read_u64().expect("faults");
let last_fault = r.read_u16().expect("last fault");
CpuState {
a,
x,
y,
s,
p,
pc,
cycles,
halted,
reset_pending,
faults,
last_fault,
}
}
#[cfg(feature = "machine-nes")]
fn peek(machine: &Machine, addr: u64) -> u8 {
use crate::core::space::MemAttrs;
use crate::core::value::Width;
machine
.space("cpubus")
.expect("cpubus")
.read(addr, Width::U8, MemAttrs::DEBUG)
.expect("open bus answers everything") as u8
}
#[cfg(feature = "machine-nes")]
#[test]
fn the_reset_vector_is_fetched_and_executed() {
let mut machine =
build_catalog("nes-ntsc", &[("cart", MINIMAL_NROM)]).expect("a minimal cart");
assert_eq!(peek(&machine, 0xfffc), 0x00);
assert_eq!(peek(&machine, 0xfffd), 0xc0);
assert_eq!(peek(&machine, 0x8000), 0x4c, "JMP at the low window");
assert_eq!(peek(&machine, 0xc000), 0x4c, "and at the high one");
let before = cpu_state(&machine, "cpu");
assert!(before.reset_pending);
machine
.run_for(crate::core::clock::GlobalTime::from_nanos(100_000))
.expect("runs");
let after = cpu_state(&machine, "cpu");
assert!(!after.reset_pending, "the reset sequence ran");
assert_eq!(
after.pc, 0xc000,
"the cpu is not executing the reset vector's target"
);
assert_eq!(after.s, 0xfd);
assert_ne!(after.p & crate::cpu::mos6502::flags::I, 0);
assert!(after.cycles >= 7 + 3, "reset plus at least one JMP");
assert_eq!(after.faults, 0, "every access is answered");
machine
.space("cpubus")
.expect("cpubus")
.write(
0x0003,
crate::core::value::Width::U8,
0xa5,
crate::core::space::MemAttrs::DEFAULT,
)
.expect("wram is writable");
for base in [0x0000u64, 0x0800, 0x1000, 0x1800] {
assert_eq!(peek(&machine, base + 3), 0xa5, "mirror at {base:#06x}");
}
}
#[cfg(feature = "machine-nes")]
#[test]
fn a_running_nes_round_trips_through_a_snapshot() {
let mut machine =
build_catalog("nes-ntsc", &[("cart", MINIMAL_NROM)]).expect("a minimal cart");
machine
.run_for(crate::core::clock::GlobalTime::from_nanos(100_000))
.expect("runs");
let saved = machine.save().expect("saves");
let hash = machine.state_hash().expect("hashes");
let mut restored =
build_catalog("nes-ntsc", &[("cart", MINIMAL_NROM)]).expect("a minimal cart");
assert_ne!(restored.state_hash().expect("hashes"), hash);
restored.load(&saved).expect("loads");
assert_eq!(restored.state_hash().expect("hashes"), hash);
assert_eq!(cpu_state(&restored, "cpu"), cpu_state(&machine, "cpu"));
let span = crate::core::clock::GlobalTime::from_nanos(1_000_000);
machine.run_for(span).expect("runs");
restored.run_for(span).expect("runs");
assert_eq!(
restored.state_hash().expect("hashes"),
machine.state_hash().expect("hashes")
);
}
#[cfg(all(feature = "machine-nes", feature = "std"))]
#[test]
fn a_real_cartridge_boots_and_executes() {
let Ok(path) = std::env::var("RSEMU_NES_TEST_ROM") else {
println!("SKIP: set RSEMU_NES_TEST_ROM to an iNES image to run this");
return;
};
let image = std::fs::read(&path).expect("RSEMU_NES_TEST_ROM is readable");
let mut machine = match build_catalog("nes-ntsc", &[("cart", &image)]) {
Ok(m) => m,
Err(e) => panic!("{path}: {e}"),
};
let before = cpu_state(&machine, "cpu");
assert!(before.reset_pending, "a cold machine owes a reset");
assert_eq!(before.cycles, 0);
let vector = u16::from(peek(&machine, 0xfffc)) | (u16::from(peek(&machine, 0xfffd)) << 8);
assert!(
vector >= 0x8000,
"a reset vector of {vector:#06x} is not in cartridge space; is the ROM mapped?"
);
let frame = crate::core::clock::GlobalTime::from_nanos(16_639_267);
machine.run_for(frame).expect("the machine runs");
let after = cpu_state(&machine, "cpu");
let domain = machine
.device("cpu")
.and_then(crate::machine::machine::DeviceEntry::domain)
.expect("the cpu has a clock domain");
let ticks = machine.clocks().ticks(domain).expect("a tick count");
println!(
"nes-ntsc + {path}:\n \
reset vector ${vector:04x}\n \
{} cpu cycles in one frame ({ticks} domain ticks)\n \
{}\n \
{} refused access(es){}",
after.cycles,
regs_line(&after),
after.faults,
if after.faults == 0 {
""
} else {
" — the memory map has a hole the open-bus policy did not cover"
},
);
assert!(!after.reset_pending, "the reset sequence must have run");
assert!(
after.cycles > 20_000,
"only {} cycles in a frame; the cpu is not running",
after.cycles
);
assert!(
after.cycles.abs_diff(ticks) <= 7,
"cpu counted {} cycles but its domain advanced {ticks}",
after.cycles
);
assert!(
!after.halted,
"a JAM opcode froze the core at ${:04x}",
after.pc
);
assert_eq!(
after.faults, 0,
"bus fault at ${:04x} after {} cycles",
after.last_fault, after.cycles
);
for _ in 0..120 {
machine.run_for(frame).expect("the machine runs");
}
let after = cpu_state(&machine, "cpu");
let ppu_domain = machine
.device("ppu")
.and_then(crate::machine::machine::DeviceEntry::domain)
.expect("the ppu has a clock domain");
let dots = machine.clocks().ticks(ppu_domain).expect("a dot count");
let ticks = machine.clocks().ticks(domain).expect("a tick count");
println!(
" after 121 frames: {} PPU dots, {}\n \
{} tiles written to the first nametable",
dots,
regs_line(&after),
nametable_tiles(&machine)
);
assert_eq!(dots, ticks * 3, "the dot clock is not three times the CPU");
assert_eq!(after.faults, 0, "bus fault at ${:04x}", after.last_fault);
assert!(
!after.halted,
"a JAM opcode froze the core at ${:04x}",
after.pc
);
let tiles = nametable_tiles(&machine);
assert!(
tiles > 64,
"only {tiles} non-blank tiles in the first nametable; the ROM never \
drew anything"
);
}
#[cfg(all(feature = "machine-nes", feature = "std"))]
fn nametable_tiles(machine: &Machine) -> usize {
use crate::core::space::MemAttrs;
use crate::core::value::Width;
let space = machine.space("ppubus").expect("ppubus");
(0..960u64)
.filter(|i| {
let tile = space
.read(0x2000 + i, Width::U8, MemAttrs::DEBUG)
.unwrap_or(0);
tile != 0x24 && tile != 0x00
})
.count()
}
#[cfg(feature = "cpu-mos6502")]
fn regs_line(s: &CpuState) -> alloc::string::String {
alloc::format!(
"A:{:02x} X:{:02x} Y:{:02x} P:{:02x} SP:{:02x} PC:{:04x}",
s.a,
s.x,
s.y,
s.p,
s.s,
s.pc
)
}
#[cfg(feature = "machine-nes")]
#[test]
fn vblank_is_reported_and_the_nmi_fires_once_a_frame() {
let image = nmi_rom();
let mut machine = build_catalog("nes-ntsc", &[("cart", &image)]).expect("a cart");
let frame = crate::core::clock::GlobalTime::from_nanos(16_639_267);
for _ in 0..6 {
machine.run_for(frame).expect("runs");
}
assert_eq!(
peek(&machine, 0x0001),
1,
"the vblank wait loop never ended"
);
let nmis = peek(&machine, 0x0000);
assert!(
(4..=6).contains(&nmis),
"{nmis} NMIs in six frames; one per vblank is the answer"
);
for _ in 0..3 {
machine.run_for(frame).expect("runs");
}
assert_eq!(peek(&machine, 0x0000), nmis + 3);
}
#[cfg(feature = "machine-nes")]
#[test]
fn a_debug_read_of_2002_advances_no_clock() {
use crate::core::space::MemAttrs;
use crate::core::value::Width;
let mut machine = build_catalog("nes-ntsc", &[("cart", MINIMAL_NROM)]).expect("a cart");
machine
.run_for(crate::core::clock::GlobalTime::from_nanos(1_000_000))
.expect("runs");
let domain = machine
.device("ppu")
.and_then(crate::machine::machine::DeviceEntry::domain)
.expect("the ppu has a clock domain");
let before = ppu_dots(&machine);
assert_eq!(before, machine.clocks().ticks(domain).expect("ticks"));
assert!(before > 0);
for _ in 0..64 {
let _ = peek(&machine, 0x2002);
}
assert_eq!(
ppu_dots(&machine),
before,
"a debug read moved the dot clock"
);
assert!(
machine
.space("cpubus")
.expect("cpubus")
.write(0x2000, Width::U8, 0x80, MemAttrs::DEBUG)
.is_err()
);
assert_eq!(ppu_dots(&machine), before);
}
#[cfg(feature = "machine-nes")]
fn ppu_dots(machine: &Machine) -> u64 {
use crate::core::state::{Migrations, Source, StateReader};
let class = &crate::dev::ppu::NES_PPU_CLASS;
let bytes = machine.save().expect("a machine saves");
let reader = StateReader::new(&bytes).expect("well formed");
let chunk = reader
.load("ppu", class.name, class.version, &Migrations::new())
.expect("a chunk per device");
chunk.reader().read_u64().expect("dots come first")
}
#[cfg(feature = "machine-nes")]
fn nmi_rom() -> alloc::vec::Vec<u8> {
let mut image = alloc::vec![0u8; 16 + 16384 + 8192];
image[..4].copy_from_slice(b"NES\x1a");
image[4] = 1; image[5] = 1; let prg = &mut image[16..16 + 16384];
let code: &[u8] = &[
0x78, 0xad, 0x02, 0x20, 0xad, 0x02, 0x20, 0x10, 0xfb, 0xad, 0x02, 0x20, 0x10, 0xfb, 0xee, 0x01, 0x00, 0xa9, 0x80, 0x8d, 0x00, 0x20, 0x4c, 0x16, 0xc0, ];
prg[..code.len()].copy_from_slice(code);
prg[0x20..0x23].copy_from_slice(&[0xe6, 0x00, 0x40]); prg[0x3ffa..0x4000].copy_from_slice(&[0x20, 0xc0, 0x00, 0xc0, 0x20, 0xc0]);
image
}
fn fixture(machine: &str, slot: &str) -> &'static [u8] {
match (machine, slot) {
(_, "cart") => MINIMAL_NROM,
#[cfg(feature = "machine-apple1")]
("apple1", "rom") => crate::dev::apple1::RSMON,
#[cfg(feature = "machine-beneater")]
("beneater-6502", "rom") => crate::dev::wdc::RSMON_IMAGE,
(m, other) => panic!("no fixture for `{m}`'s media slot `{other}`"),
}
}
static MINIMAL_NROM: &[u8] = &{
let mut image = [0u8; 16 + 16384 + 8192];
image[0] = b'N';
image[1] = b'E';
image[2] = b'S';
image[3] = 0x1a;
image[4] = 1; image[5] = 1; image[16 + 0x3ffc] = 0x00;
image[16 + 0x3ffd] = 0xc0;
image[16] = 0x4c;
image[17] = 0x00;
image[18] = 0xc0;
image
};
}