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"),
};
#[cfg(feature = "machine-riscv-virt")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-riscv-virt")))]
pub static RISCV_VIRT: CatalogEntry = CatalogEntry {
name: "riscv-virt",
summary: "RISC-V `virt`: RV64GC hart, CLINT, PLIC, 16550, virtio-MMIO, NOR flash, DTB",
media: &["firmware", "flash0", "flash1", "initrd", "disk"],
source: include_str!("../../machines/riscv-virt.machine"),
};
#[cfg(feature = "machine-gameboy")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-gameboy")))]
pub static GAMEBOY: CatalogEntry = CatalogEntry {
name: "gameboy",
summary: "Nintendo Game Boy (DMG, 1989): SM83 at 4.194304 MHz, 160x144 LCD",
media: &["cart"],
source: include_str!("../../machines/gameboy.machine"),
};
#[cfg(feature = "machine-sms")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-sms")))]
pub static SMS_NTSC: CatalogEntry = CatalogEntry {
name: "sms-ntsc",
summary: "Sega Master System (NTSC): Z80 at 3.58 MHz, 315-5124 VDP, SN76489, 262 lines",
media: &["cart"],
source: include_str!("../../machines/sms-ntsc.machine"),
};
#[cfg(feature = "machine-sms")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-sms")))]
pub static SMS_PAL: CatalogEntry = CatalogEntry {
name: "sms-pal",
summary: "Sega Master System (PAL): Z80 at 3.55 MHz, 315-5246 VDP, SN76489, 313 lines",
media: &["cart"],
source: include_str!("../../machines/sms-pal.machine"),
};
#[cfg(feature = "machine-spi-panel")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-spi-panel")))]
pub static SPI_PANEL: CatalogEntry = CatalogEntry {
name: "spi-panel",
summary: "a minimal SoC-shaped board: RV32, SPI, a Sitronix ST7272A panel and a scanout engine",
media: &["firmware"],
source: include_str!("../../machines/spi-panel.machine"),
};
#[cfg(feature = "machine-spi-flash")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-spi-flash")))]
pub static SPI_FLASH: CatalogEntry = CatalogEntry {
name: "spi-flash",
summary: "a serial-flash board: RV32, an STM32 SPI master, an OCTOSPI window and a W25Q part",
media: &["firmware"],
source: include_str!("../../machines/spi-flash.machine"),
};
#[cfg(feature = "machine-arm926")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-arm926")))]
pub static ARM926: CatalogEntry = CatalogEntry {
name: "arm926",
summary: "a bare ARM926EJ-S SoC skeleton: ARMv5TE core, boot ROM, DRAM, one peripheral window",
media: &["firmware"],
source: include_str!("../../machines/arm926.machine"),
};
#[cfg(feature = "machine-pc-at")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-pc-at")))]
pub static PC_AT: CatalogEntry = CatalogEntry {
name: "pc-at",
summary: "IBM PC/AT-class board: x86, 8259As, 8254, MC146818, 8042, 8237s, VGA, floppy, IDE",
media: &["bios", "vgabios", "floppy", "hd0", "hd1"],
source: include_str!("../../machines/pc-at.machine"),
};
#[cfg(feature = "machine-pc-apic")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-pc-apic")))]
pub static PC_APIC: CatalogEntry = CatalogEntry {
name: "pc-apic",
summary: "PC/AT interrupt path with the APIC fitted: x86, 8259As, local APIC, I/O APIC, HPET",
media: &["bios"],
source: include_str!("../../machines/pc-apic.machine"),
};
#[cfg(feature = "machine-stm32f407")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-stm32f407")))]
pub static STM32F407: CatalogEntry = CatalogEntry {
name: "stm32f407",
summary: "STM32F407VGT6: Cortex-M4 at 168 MHz, 1 MiB flash, three SRAM banks, six GPIO ports, USART2",
media: &["firmware"],
source: include_str!("../../machines/stm32f407.machine"),
};
#[cfg(feature = "machine-z80-mini")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-z80-mini")))]
pub static Z80_MINI: CatalogEntry = CatalogEntry {
name: "z80-mini",
summary: "a minimal Z80 board: ROM, RAM, and the separate 64 KiB port space IN and OUT reach",
media: &["firmware"],
source: include_str!("../../machines/z80-mini.machine"),
};
#[cfg(feature = "machine-ne2k-mini")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-ne2k-mini")))]
pub static NE2K_MINI: CatalogEntry = CatalogEntry {
name: "ne2k-mini",
summary: "a minimal Z80 board with an NE2000 Ethernet card on its port bus",
media: &["firmware"],
source: include_str!("../../machines/ne2k-mini.machine"),
};
#[cfg(feature = "machine-ahci-mini")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-ahci-mini")))]
pub static AHCI_MINI: CatalogEntry = CatalogEntry {
name: "ahci-mini",
summary: "a minimal PCI board with an AHCI adapter and one SATA drive: RAM, a host bridge, \
an 8259A",
media: &["sata0"],
source: include_str!("../../machines/ahci-mini.machine"),
};
#[cfg(feature = "machine-nvme-mini")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-nvme-mini")))]
pub static NVME_MINI: CatalogEntry = CatalogEntry {
name: "nvme-mini",
summary: "a minimal PCI board with an NVM Express controller: RAM, a host bridge, an 8259A",
media: &["nvme0"],
source: include_str!("../../machines/nvme-mini.machine"),
};
#[cfg(feature = "machine-m68k-mini")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-m68k-mini")))]
pub static M68K_MINI: CatalogEntry = CatalogEntry {
name: "m68k-mini",
summary: "a minimal MC68000 board: 8 MHz, a big-endian 24-bit space, ROM at the vectors, RAM",
media: &["firmware"],
source: include_str!("../../machines/m68k-mini.machine"),
};
#[cfg(feature = "machine-mips-mini")]
#[cfg_attr(docsrs, doc(cfg(feature = "machine-mips-mini")))]
pub static MIPS_MINI: CatalogEntry = CatalogEntry {
name: "mips-mini",
summary: "a minimal MIPS R3000A board: 25 MHz, a physical 32-bit space, a kseg1 boot ROM, RAM",
media: &["firmware"],
source: include_str!("../../machines/mips-mini.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-arm926")]
out.push(&ARM926);
#[cfg(feature = "machine-beneater")]
out.push(&BENEATER_6502);
#[cfg(feature = "machine-gameboy")]
out.push(&GAMEBOY);
#[cfg(feature = "machine-m68k-mini")]
out.push(&M68K_MINI);
#[cfg(feature = "machine-mips-mini")]
out.push(&MIPS_MINI);
#[cfg(feature = "machine-ne2k-mini")]
out.push(&NE2K_MINI);
#[cfg(feature = "machine-ahci-mini")]
out.push(&AHCI_MINI);
#[cfg(feature = "machine-nvme-mini")]
out.push(&NVME_MINI);
#[cfg(feature = "machine-pc-at")]
out.push(&PC_AT);
#[cfg(feature = "machine-pc-apic")]
out.push(&PC_APIC);
#[cfg(feature = "machine-nes")]
out.push(&NES_NTSC);
#[cfg(feature = "machine-nes")]
out.push(&NES_PAL);
#[cfg(feature = "machine-riscv-virt")]
out.push(&RISCV_VIRT);
#[cfg(feature = "machine-sms")]
out.push(&SMS_NTSC);
#[cfg(feature = "machine-sms")]
out.push(&SMS_PAL);
#[cfg(feature = "machine-spi-panel")]
out.push(&SPI_PANEL);
#[cfg(feature = "machine-spi-flash")]
out.push(&SPI_FLASH);
#[cfg(feature = "machine-stm32f407")]
out.push(&STM32F407);
#[cfg(feature = "machine-z80-mini")]
out.push(&Z80_MINI);
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 = "cpu-sm83")]
crate::cpu::sm83::register(&mut reg)?;
#[cfg(feature = "dev-gb")]
crate::dev::gb::register(&mut reg)?;
#[cfg(feature = "dev-sms")]
crate::dev::sms::register(&mut reg)?;
#[cfg(feature = "dev-nes-cart")]
crate::dev::cart::nrom::register(&mut reg)?;
#[cfg(feature = "dev-nes-io")]
crate::dev::nes::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-at24c")]
crate::dev::atmel::register(&mut reg)?;
#[cfg(feature = "dev-apple1")]
crate::dev::apple1::register(&mut reg)?;
#[cfg(feature = "cpu-arm-aprofile")]
crate::cpu::arm::aprofile::register(&mut reg)?;
#[cfg(feature = "cpu-arm-v7m")]
crate::cpu::arm::v7m::register(&mut reg)?;
#[cfg(feature = "cpu-z80")]
crate::cpu::z80::register(&mut reg)?;
#[cfg(feature = "cpu-m68k")]
crate::cpu::m68k::register(&mut reg)?;
#[cfg(feature = "cpu-mips")]
crate::cpu::mips::register(&mut reg)?;
#[cfg(feature = "cpu-riscv")]
crate::cpu::riscv::register(&mut reg)?;
#[cfg(feature = "dev-riscv")]
crate::dev::riscv::register(&mut reg)?;
#[cfg(any(feature = "dev-flash-cfi", feature = "dev-flash-spinor"))]
crate::dev::flash::register(&mut reg)?;
#[cfg(feature = "dev-sd-card")]
crate::dev::sd::register(&mut reg)?;
#[cfg(feature = "dev-ata-disk")]
crate::dev::ata::register(&mut reg)?;
#[cfg(feature = "dev-wdc")]
crate::dev::wdc::register(&mut reg)?;
#[cfg(feature = "dev-ne2000")]
crate::dev::net::ne2000::register(&mut reg)?;
#[cfg(feature = "dev-ahci")]
crate::dev::ahci::register(&mut reg)?;
#[cfg(feature = "dev-nvme")]
crate::dev::nvme::register(&mut reg)?;
#[cfg(feature = "cpu-x86")]
crate::cpu::x86::register(&mut reg)?;
#[cfg(feature = "dev-pc")]
crate::dev::pc::register(&mut reg)?;
#[cfg(feature = "bus-spi")]
crate::bus::spi::controller::register(&mut reg)?;
#[cfg(feature = "dev-st7272a")]
crate::dev::sitronix::register(&mut reg)?;
#[cfg(any(
feature = "dev-stm32",
feature = "dev-stm32-sdmmc",
feature = "dev-stm32-spi",
feature = "dev-stm32-octospi",
feature = "dev-stm32-i2c",
feature = "machine-spi-flash"
))]
crate::dev::stm32::register(&mut reg)?;
#[cfg(feature = "dev-lcdc")]
crate::dev::lcd::register(&mut reg)?;
#[cfg(any(
feature = "dev-usb-ehci",
feature = "dev-usb-chipidea",
feature = "dev-usb-dwc2",
feature = "dev-usb-hid"
))]
crate::dev::usb::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 = "cpu-sm83")]
crate::cpu::sm83::bind(&mut b)?;
#[cfg(feature = "dev-gb")]
crate::dev::gb::bind(&mut b)?;
#[cfg(feature = "dev-sms")]
crate::dev::sms::bind(&mut b)?;
#[cfg(feature = "dev-nes-cart")]
crate::dev::cart::nrom::bind(&mut b)?;
#[cfg(feature = "dev-nes-io")]
crate::dev::nes::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-at24c")]
crate::dev::atmel::bind(&mut b)?;
#[cfg(feature = "dev-apple1")]
crate::dev::apple1::bind(&mut b)?;
#[cfg(feature = "cpu-arm-aprofile")]
crate::cpu::arm::aprofile::bind(&mut b)?;
#[cfg(feature = "cpu-arm-v7m")]
crate::cpu::arm::v7m::bind(&mut b)?;
#[cfg(feature = "cpu-z80")]
crate::cpu::z80::bind(&mut b)?;
#[cfg(feature = "cpu-m68k")]
crate::cpu::m68k::bind(&mut b)?;
#[cfg(feature = "cpu-mips")]
crate::cpu::mips::bind(&mut b)?;
#[cfg(feature = "cpu-riscv")]
crate::cpu::riscv::bind(&mut b)?;
#[cfg(feature = "dev-riscv")]
crate::dev::riscv::bind(&mut b)?;
#[cfg(any(feature = "dev-flash-cfi", feature = "dev-flash-spinor"))]
crate::dev::flash::bind(&mut b)?;
#[cfg(feature = "dev-sd-card")]
crate::dev::sd::bind(&mut b)?;
#[cfg(feature = "dev-ata-disk")]
crate::dev::ata::bind(&mut b)?;
#[cfg(feature = "dev-wdc")]
crate::dev::wdc::bind(&mut b)?;
#[cfg(feature = "dev-ne2000")]
crate::dev::net::ne2000::bind(&mut b)?;
#[cfg(feature = "dev-ahci")]
crate::dev::ahci::bind(&mut b)?;
#[cfg(feature = "dev-nvme")]
crate::dev::nvme::bind(&mut b)?;
#[cfg(feature = "cpu-x86")]
crate::cpu::x86::bind(&mut b)?;
#[cfg(feature = "dev-pc")]
crate::dev::pc::bind(&mut b)?;
#[cfg(feature = "bus-spi")]
crate::bus::spi::controller::bind(&mut b)?;
#[cfg(feature = "dev-st7272a")]
crate::dev::sitronix::bind(&mut b)?;
#[cfg(any(
feature = "dev-stm32",
feature = "dev-stm32-sdmmc",
feature = "dev-stm32-spi",
feature = "dev-stm32-octospi",
feature = "dev-stm32-i2c",
feature = "machine-spi-flash"
))]
crate::dev::stm32::bind(&mut b)?;
#[cfg(feature = "dev-lcdc")]
crate::dev::lcd::bind(&mut b)?;
#[cfg(any(
feature = "dev-usb-ehci",
feature = "dev-usb-chipidea",
feature = "dev-usb-dwc2",
feature = "dev-usb-hid"
))]
crate::dev::usb::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 = "cpu-sm83")]
table.insert(crate::cpu::sm83::schema());
#[cfg(feature = "dev-gb")]
for schema in crate::dev::gb::schemas() {
table.insert(schema);
}
#[cfg(feature = "dev-sms")]
for schema in crate::dev::sms::schemas() {
table.insert(schema);
}
#[cfg(feature = "dev-nes-cart")]
table.insert(crate::dev::cart::nrom::schema());
#[cfg(feature = "dev-nes-io")]
for schema in crate::dev::nes::schemas() {
table.insert(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-at24c")]
for schema in crate::dev::atmel::schemas() {
table.insert(schema);
}
#[cfg(feature = "dev-apple1")]
for schema in crate::dev::apple1::schemas() {
table.insert(schema);
}
#[cfg(feature = "cpu-arm-aprofile")]
table.insert(crate::cpu::arm::aprofile::schema());
#[cfg(feature = "cpu-arm-v7m")]
table.insert(crate::cpu::arm::v7m::schema());
#[cfg(feature = "cpu-z80")]
table.insert(crate::cpu::z80::schema());
#[cfg(feature = "dev-ne2000")]
table.insert(crate::dev::net::ne2000::schema());
#[cfg(feature = "dev-ahci")]
table.insert(crate::dev::ahci::schema());
#[cfg(feature = "dev-nvme")]
table.insert(crate::dev::nvme::schema());
#[cfg(feature = "cpu-m68k")]
table.insert(crate::cpu::m68k::schema());
#[cfg(feature = "cpu-mips")]
table.insert(crate::cpu::mips::schema());
#[cfg(feature = "cpu-riscv")]
table.insert(crate::cpu::riscv::schema());
#[cfg(feature = "dev-riscv")]
for schema in crate::dev::riscv::schemas() {
table.insert(schema);
}
#[cfg(any(feature = "dev-flash-cfi", feature = "dev-flash-spinor"))]
for schema in crate::dev::flash::schemas() {
table.insert(schema);
}
#[cfg(feature = "dev-sd-card")]
for schema in crate::dev::sd::schemas() {
table.insert(schema);
}
#[cfg(feature = "dev-ata-disk")]
for schema in crate::dev::ata::schemas() {
table.insert(schema);
}
#[cfg(feature = "dev-wdc")]
for schema in crate::dev::wdc::schemas() {
table.insert(schema);
}
#[cfg(feature = "bus-spi")]
table.insert(crate::bus::spi::controller::schema());
#[cfg(feature = "dev-st7272a")]
for schema in crate::dev::sitronix::schemas() {
table.insert(schema);
}
#[cfg(any(
feature = "dev-stm32",
feature = "dev-stm32-sdmmc",
feature = "dev-stm32-spi",
feature = "dev-stm32-octospi",
feature = "dev-stm32-i2c",
feature = "machine-spi-flash"
))]
for schema in crate::dev::stm32::schemas() {
table.insert(schema);
}
#[cfg(feature = "cpu-x86")]
for schema in crate::cpu::x86::schemas() {
table.insert(schema);
}
#[cfg(feature = "dev-pc")]
for schema in crate::dev::pc::schemas() {
table.insert(schema);
}
#[cfg(feature = "dev-lcdc")]
for schema in crate::dev::lcd::schemas() {
table.insert(schema);
}
#[cfg(any(
feature = "dev-usb-ehci",
feature = "dev-usb-chipidea",
feature = "dev-usb-dwc2",
feature = "dev-usb-hid"
))]
for schema in crate::dev::usb::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> {
build_catalog_with_hosts(name, media).map(|(machine, _)| machine)
}
pub fn build_catalog_with_hosts(
name: &str,
media: &[(&str, &[u8])],
) -> Result<(Machine, alloc::sync::Arc<crate::core::hosts::HostObjects>)> {
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);
}
let machine = crate::machine::build(entry.name, entry.source, ®istry()?, &options)?;
Ok((machine, options.realize.hosts))
}
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("megadrive", &[])
.expect_err("no megadrive")
.to_string();
assert!(e.contains("megadrive"), "{e}");
}
#[cfg(feature = "cpu-mos6502")]
#[allow(dead_code)]
#[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")]
#[allow(dead_code)]
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")]
#[allow(dead_code)]
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!(
(3..=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(all(feature = "machine-nes", feature = "dev-nes-ppu"))]
#[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(all(feature = "machine-nes", feature = "dev-nes-ppu"))]
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) {
#[cfg(feature = "machine-gameboy")]
("gameboy", "cart") => minimal_gb(),
(_, "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,
#[cfg(feature = "machine-riscv-virt")]
("riscv-virt", "firmware") => &[0x73, 0x00, 0x50, 0x10, 0x6f, 0xf0, 0xdf, 0xff],
#[cfg(feature = "machine-riscv-virt")]
("riscv-virt", "flash0" | "flash1") => &[],
#[cfg(feature = "machine-riscv-virt")]
("riscv-virt", "initrd") => &[],
#[cfg(feature = "machine-riscv-virt")]
("riscv-virt", "disk") => &[],
#[cfg(feature = "machine-spi-panel")]
("spi-panel", "firmware") => crate::dev::lcd::demo::PANEL_DEMO,
#[cfg(feature = "machine-spi-flash")]
("spi-flash", "firmware") => crate::dev::stm32::demo::SPI_FLASH_DEMO,
#[cfg(feature = "machine-arm926")]
("arm926", "firmware") => &[0xfe, 0xff, 0xff, 0xea],
#[cfg(feature = "machine-stm32f407")]
("stm32f407", "firmware") => &[
0x00, 0x00, 0x02, 0x20, 0x09, 0x00, 0x00, 0x00, 0xfe, 0xe7, ],
#[cfg(feature = "machine-z80-mini")]
("z80-mini", "firmware") => &[0x18, 0xfe],
#[cfg(feature = "machine-ne2k-mini")]
("ne2k-mini", "firmware") => &[0x18, 0xfe],
#[cfg(feature = "machine-nvme-mini")]
("nvme-mini", "nvme0") => &[],
#[cfg(feature = "machine-ahci-mini")]
("ahci-mini", "sata0") => &[],
#[cfg(feature = "machine-mips-mini")]
("mips-mini", "firmware") => &[
0x00, 0x00, 0xf0, 0x0b, 0x00, 0x00, 0x00, 0x00, ],
#[cfg(feature = "machine-m68k-mini")]
("m68k-mini", "firmware") => &[
0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x60, 0xfe, ],
#[cfg(feature = "machine-pc-at")]
("pc-at", "bios") => blank(128 * 1024),
#[cfg(feature = "machine-pc-at")]
("pc-at", "vgabios") => blank(32 * 1024),
#[cfg(feature = "machine-pc-at")]
("pc-at", "floppy") => blank(1_474_560),
#[cfg(feature = "machine-pc-at")]
("pc-at", "hd0" | "hd1") => &[],
#[cfg(feature = "machine-pc-apic")]
("pc-apic", "bios") => blank(128 * 1024),
(m, other) => panic!("no fixture for `{m}`'s media slot `{other}`"),
}
}
#[cfg(any(feature = "machine-pc-at", feature = "machine-pc-apic"))]
fn blank(len: usize) -> &'static [u8] {
use crate::core::sync::Global;
use alloc::collections::BTreeMap;
static CACHE: Global<BTreeMap<usize, &'static [u8]>> = Global::new(BTreeMap::new());
let mut cache = CACHE.lock();
if let Some(image) = cache.get(&len).copied() {
return image;
}
let image: &'static [u8] = alloc::vec![0u8; len].leak();
cache.insert(len, image);
image
}
#[cfg(feature = "machine-gameboy")]
fn minimal_gb() -> &'static [u8] {
use crate::core::sync::Global;
static IMAGE: Global<Option<&'static [u8]>> = Global::new(None);
let mut slot = IMAGE.lock();
if let Some(image) = *slot {
return image;
}
let image: &'static [u8] =
crate::dev::gb::cart::synthetic_image(2, 0x00, 0x00, &[0x18, 0xfe]).leak();
*slot = Some(image);
image
}
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
};
}