#![allow(dead_code)]
use std::collections::BTreeMap;
use rsemu::core::clock::GlobalTime;
use rsemu::host::display::{PixelFormat, Scanout, Surface};
use rsemu::machine::Machine;
pub(crate) const NES_ROM_ENV: &str = "RSEMU_BENCH_NES_ROM";
pub(crate) fn nes_rom_override() -> Option<String> {
std::env::var(NES_ROM_ENV).ok().filter(|p| !p.is_empty())
}
const NOMINAL_FRAME_NS: u64 = 16_666_667;
pub(crate) struct Workload {
pub(crate) name: &'static str,
pub(crate) what: &'static str,
pub(crate) frames: u32,
pub(crate) checkpoint_every: u32,
build: fn() -> Booted,
}
pub(crate) struct Booted {
pub(crate) machine: Machine,
pub(crate) capture: Option<Capture>,
span: GlobalTime,
}
impl Booted {
fn wrap(machine: Machine, scanout: Option<Box<dyn Scanout>>) -> Booted {
let capture = scanout.map(Capture::new);
let ns = capture
.as_ref()
.map(Capture::frame_period_ns)
.filter(|ns| *ns != 0)
.unwrap_or(NOMINAL_FRAME_NS);
Booted {
machine,
capture,
span: GlobalTime::from_nanos(ns),
}
}
pub(crate) fn step(&mut self) {
self.machine.run_for(self.span).expect("the machine runs");
}
pub(crate) fn step_many(&mut self, frames: u32) {
for _ in 0..frames {
self.step();
}
}
pub(crate) fn frame_period_ns(&self) -> u64 {
self.span.as_nanos()
}
}
#[allow(clippy::vec_init_then_push, unused_mut)]
pub(crate) fn all() -> Vec<Workload> {
let mut out: Vec<Workload> = Vec::new();
#[cfg(all(feature = "machine-nes", feature = "dev-nes-ppu"))]
out.push(Workload {
name: "nes-ntsc",
what: "full-screen background, 64 sprites, scrolling, APU on, \
a 256-byte read-modify-write loop in WRAM",
frames: 60,
checkpoint_every: 15,
build: boot_nes,
});
#[cfg(feature = "machine-gameboy")]
out.push(Workload {
name: "gameboy",
what: "LCD on with a filled tile map, scrolling, \
a 4 KiB read-modify-write loop in WRAM",
frames: 60,
checkpoint_every: 15,
build: boot_gameboy,
});
#[cfg(feature = "machine-apple1")]
out.push(Workload {
name: "apple1",
what: "RSMON at its prompt: a 6502 polling the PIA, no display device",
frames: 60,
checkpoint_every: 30,
build: boot_apple1,
});
#[cfg(feature = "machine-riscv-virt")]
out.push(Workload {
name: "riscv-virt",
what: "an RV64I integer loop through DRAM: add, store, load, shift",
frames: 60,
checkpoint_every: 30,
build: boot_riscv_virt,
});
out
}
impl Workload {
pub(crate) fn boot(&self) -> Booted {
(self.build)()
}
pub(crate) fn run<F>(&self, frames: u32, mut checkpoint: F)
where
F: FnMut(u32, &mut Booted) -> bool,
{
let mut booted = self.boot();
for frame in 1..=frames {
booted.step();
if !checkpoint(frame, &mut booted) {
break;
}
}
}
}
pub(crate) struct Capture {
scanout: Box<dyn Scanout>,
surface: Surface,
}
impl Capture {
fn new(scanout: Box<dyn Scanout>) -> Capture {
let info = scanout.info();
let surface = Surface::new(PixelFormat::RGBA8888, info.width, info.height);
Capture { scanout, surface }
}
fn frame_period_ns(&self) -> u64 {
self.scanout.frame_period_ns()
}
pub(crate) fn frame_counter(&self) -> u64 {
self.scanout.frame_counter()
}
pub(crate) fn hash(&mut self) -> u64 {
self.scanout.capture(&mut self.surface);
self.surface.hash()
}
pub(crate) fn surface(&self) -> &Surface {
&self.surface
}
pub(crate) fn distinct_colours(&mut self) -> usize {
self.scanout.capture(&mut self.surface);
let mut seen: BTreeMap<[u8; 4], ()> = BTreeMap::new();
for pixel in self.surface.pixels().as_chunks::<4>().0 {
seen.insert(*pixel, ());
}
seen.len()
}
}
#[cfg(all(feature = "machine-nes", feature = "dev-nes-ppu"))]
fn boot_nes() -> Booted {
use rsemu::host::display::nes::capture;
use rsemu::machine::catalog;
let image = nes_rom();
let entry = catalog::machine("nes-ntsc").expect("this build ships nes-ntsc");
let mut options = catalog::build_options().expect("the catalog agrees with itself");
options.realize.media.insert("cart", image.as_slice());
capture::install(&mut options).expect("the interception installs");
let registry = catalog::registry().expect("a registry");
let machine = rsemu::machine::build(entry.name, entry.source, ®istry, &options)
.expect("the NES realizes");
let scanout = capture::take(&options.realize.hosts).expect("the machine has a PPU");
Booted::wrap(machine, Some(Box::new(scanout)))
}
#[cfg(feature = "machine-gameboy")]
fn boot_gameboy() -> Booted {
use rsemu::machine::catalog;
let image = rsemu::dev::gb::cart::synthetic_image(2, 0x00, 0x00, GAMEBOY_PROGRAM);
let machine = catalog::build_catalog("gameboy", &[("cart", &image)]).expect("the GB realizes");
Booted::wrap(machine, None)
}
#[cfg(feature = "machine-apple1")]
fn boot_apple1() -> Booted {
use rsemu::machine::catalog;
let machine = catalog::build_catalog("apple1", &[("rom", rsemu::dev::apple1::RSMON)])
.expect("the Apple 1 realizes");
Booted::wrap(machine, None)
}
#[cfg(feature = "machine-riscv-virt")]
fn boot_riscv_virt() -> Booted {
use rsemu::machine::catalog;
let firmware = riscv_firmware();
let entry = catalog::machine("riscv-virt").expect("this build ships riscv-virt");
let mut options = catalog::build_options().expect("the catalog agrees with itself");
options
.realize
.media
.insert("firmware", firmware.as_slice());
options.realize.media.insert("flash0", &[][..]);
options.realize.media.insert("flash1", &[][..]);
options.realize.media.insert("disk", &[][..]);
options.realize.media.insert("initrd", &[][..]);
options
.resolve
.params
.push((String::from("ram"), String::from("16M")));
let registry = catalog::registry().expect("a registry");
let machine = rsemu::machine::build(entry.name, entry.source, ®istry, &options)
.expect("the virt board realizes");
Booted::wrap(machine, None)
}
#[cfg(all(feature = "machine-nes", feature = "dev-nes-ppu"))]
fn nes_rom() -> Vec<u8> {
if let Some(path) = nes_rom_override() {
return std::fs::read(&path).unwrap_or_else(|e| panic!("{NES_ROM_ENV}={path}: {e}"));
}
let mut asm = Asm6502::new(0xc000);
asm.emit(&[0x78]); asm.emit(&[0xd8]); asm.emit(&[0xa2, 0xff]); asm.emit(&[0x9a]); asm.emit(&[0xa9, 0x00]); asm.emit(&[0x8d, 0x00, 0x20]); asm.emit(&[0x8d, 0x01, 0x20]);
asm.label("vbl1");
asm.emit(&[0x2c, 0x02, 0x20]); asm.branch(0x10, "vbl1"); asm.label("vbl2");
asm.emit(&[0x2c, 0x02, 0x20]); asm.branch(0x10, "vbl2");
asm.emit(&[0xa9, 0x3f]); asm.emit(&[0x8d, 0x06, 0x20]); asm.emit(&[0xa9, 0x00]); asm.emit(&[0x8d, 0x06, 0x20]); asm.emit(&[0xa2, 0x00]); asm.label("pal");
asm.emit(&[0x8a]); asm.emit(&[0x8d, 0x07, 0x20]); asm.emit(&[0xe8]); asm.emit(&[0xe0, 0x20]); asm.branch(0xd0, "pal");
asm.emit(&[0xa9, 0x20]); asm.emit(&[0x8d, 0x06, 0x20]); asm.emit(&[0xa9, 0x00]); asm.emit(&[0x8d, 0x06, 0x20]); asm.emit(&[0xa2, 0x04]); asm.emit(&[0xa0, 0x00]); asm.label("nt");
asm.emit(&[0x98]); asm.emit(&[0x8d, 0x07, 0x20]); asm.emit(&[0xc8]); asm.branch(0xd0, "nt"); asm.emit(&[0xca]); asm.branch(0xd0, "nt");
asm.emit(&[0xa9, 0x00]); asm.emit(&[0x8d, 0x03, 0x20]); asm.emit(&[0xa2, 0x00]); asm.label("oam");
asm.emit(&[0x8a]); asm.emit(&[0x8d, 0x04, 0x20]); asm.emit(&[0xe8]); asm.branch(0xd0, "oam");
for (reg, value) in [
(0x4015u16, 0x0fu8), (0x4000, 0xbf), (0x4001, 0x08), (0x4002, 0x40),
(0x4003, 0x08),
(0x4004, 0x7f), (0x4005, 0x08),
(0x4006, 0x91),
(0x4007, 0x08),
(0x4008, 0xff), (0x400a, 0x30),
(0x400b, 0x08),
(0x400c, 0x3f), (0x400e, 0x05),
(0x400f, 0x08),
] {
asm.emit(&[0xa9, value]);
asm.emit(&[0x8d, (reg & 0xff) as u8, (reg >> 8) as u8]);
}
asm.emit(&[0xa9, 0x1e]); asm.emit(&[0x8d, 0x01, 0x20]); asm.emit(&[0xa9, 0x90]); asm.emit(&[0x8d, 0x00, 0x20]);
asm.label("main");
asm.emit(&[0xa2, 0x00]); asm.label("work");
asm.emit(&[0xbd, 0x00, 0x02]); asm.emit(&[0x18]); asm.emit(&[0x69, 0x07]); asm.emit(&[0x9d, 0x00, 0x02]); asm.emit(&[0xe8]); asm.branch(0xd0, "work"); asm.emit(&[0x4c, 0x00, 0x00]); let jmp_operand = asm.here() - 2;
asm.label("nmi");
asm.emit(&[0x48]); asm.emit(&[0x2c, 0x02, 0x20]); asm.emit(&[0xa5, 0x10]); asm.emit(&[0x8d, 0x05, 0x20]); asm.emit(&[0xa9, 0x00]); asm.emit(&[0x8d, 0x05, 0x20]); asm.emit(&[0xe6, 0x10]); asm.emit(&[0x68]); asm.emit(&[0x40]); asm.label("irq");
asm.emit(&[0x40]);
let (mut prg, labels) = asm.finish();
let main = labels["main"];
prg[jmp_operand] = (main & 0xff) as u8;
prg[jmp_operand + 1] = (main >> 8) as u8;
assert!(prg.len() < 0x3ffa, "the program collides with the vectors");
prg.resize(0x4000, 0xea);
let vector = |name: &str| {
let addr = labels[name];
[(addr & 0xff) as u8, (addr >> 8) as u8]
};
prg[0x3ffa..0x3ffc].copy_from_slice(&vector("nmi"));
prg[0x3ffc..0x3ffe].copy_from_slice(&[0x00, 0xc0]);
prg[0x3ffe..0x4000].copy_from_slice(&vector("irq"));
let mut chr = vec![0u8; 8192];
for (i, byte) in chr.iter_mut().enumerate() {
let i = i as u32;
*byte =
((i.wrapping_mul(37) ^ (i >> 4).wrapping_mul(151)).wrapping_add(i >> 9) & 0xff) as u8;
}
let mut image = Vec::with_capacity(16 + prg.len() + chr.len());
image.extend_from_slice(&[b'N', b'E', b'S', 0x1a, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
image.extend_from_slice(&prg);
image.extend_from_slice(&chr);
image
}
#[cfg(all(feature = "machine-nes", feature = "dev-nes-ppu"))]
struct Asm6502 {
org: u16,
bytes: Vec<u8>,
labels: BTreeMap<&'static str, u16>,
fixups: Vec<(usize, &'static str)>,
}
#[cfg(all(feature = "machine-nes", feature = "dev-nes-ppu"))]
impl Asm6502 {
fn new(org: u16) -> Asm6502 {
Asm6502 {
org,
bytes: Vec::new(),
labels: BTreeMap::new(),
fixups: Vec::new(),
}
}
fn pc(&self) -> u16 {
self.org.wrapping_add(self.bytes.len() as u16)
}
fn here(&self) -> usize {
self.bytes.len()
}
fn label(&mut self, name: &'static str) {
let pc = self.pc();
self.labels.insert(name, pc);
}
fn emit(&mut self, bytes: &[u8]) {
self.bytes.extend_from_slice(bytes);
}
fn branch(&mut self, opcode: u8, target: &'static str) {
self.bytes.push(opcode);
let at = self.bytes.len();
self.bytes.push(0);
self.fixups.push((at, target));
}
fn finish(mut self) -> (Vec<u8>, BTreeMap<&'static str, u16>) {
for (at, target) in &self.fixups {
let to = i32::from(*self.labels.get(target).expect("a branch to a real label"));
let from = i32::from(self.org) + *at as i32 + 1;
let offset = to - from;
assert!(
(-128..=127).contains(&offset),
"branch to `{target}` is {offset} bytes, out of a 6502 branch's reach"
);
self.bytes[*at] = offset as i8 as u8;
}
(self.bytes, self.labels)
}
}
#[cfg(feature = "machine-gameboy")]
const GAMEBOY_PROGRAM: &[u8] = &[
0x21, 0x00, 0x80, 0x0e, 0x00, 0x3e, 0x00, 0x77, 0x3c, 0x23, 0x0d, 0x20, 0xfa, 0x21, 0x00, 0x98, 0x0e, 0x00, 0x3e, 0x00, 0x77, 0x3c, 0x23, 0x0d, 0x20, 0xfa, 0x3e, 0xe4, 0xe0, 0x47, 0x3e, 0x91, 0xe0, 0x40, 0x21, 0x00, 0xc0, 0x7e, 0xc6, 0x07, 0x77, 0x23, 0x7c, 0xfe, 0xd0, 0x20, 0xf6, 0xf0, 0x42, 0x3c, 0xe0, 0x42, 0x18, 0xec, ];
#[cfg(feature = "machine-riscv-virt")]
fn riscv_firmware() -> Vec<u8> {
const PROGRAM: [u32; 12] = [
0x0000_0f17, 0x014f_0f13, 0x0000_1397, 0x0000_0293, 0x0010_0313, 0x0062_82b3, 0x0053_b023, 0x0003_be03, 0x003e_1e93, 0x003e_de93, 0x01d2_82b3, 0x000f_0067, ];
PROGRAM.iter().flat_map(|w| w.to_le_bytes()).collect()
}
pub(crate) const GOLDEN_PATH: &str = "tests/goldens/frame-hashes.txt";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Golden {
pub(crate) frame: u32,
pub(crate) state: u64,
pub(crate) frame_hash: Option<u64>,
}
pub(crate) fn goldens() -> BTreeMap<String, Vec<Golden>> {
let path = golden_file();
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let mut out: BTreeMap<String, Vec<Golden>> = BTreeMap::new();
for (n, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let mut field = line.split_whitespace();
let mut next = |what: &str| {
field
.next()
.unwrap_or_else(|| panic!("{}:{}: expected {what}", path.display(), n + 1))
};
let name = next("a workload name").to_owned();
let frame = next("a frame number").parse().expect("a frame number");
let state = parse_hash(next("a state hash"));
let frame_hash = match next("a frame hash or `-`") {
"-" => None,
hex => Some(parse_hash(hex)),
};
out.entry(name).or_default().push(Golden {
frame,
state,
frame_hash,
});
}
out
}
pub(crate) fn bless(fresh: &BTreeMap<String, Vec<Golden>>) {
let mut merged = goldens();
for (name, rows) in fresh {
merged.insert(name.clone(), rows.clone());
}
let mut text = String::from(GOLDEN_HEADER);
for (name, rows) in &merged {
for row in rows {
let frame_hash = match row.frame_hash {
Some(h) => format!("{h:#018x}"),
None => String::from("-"),
};
text.push_str(&format!(
"{name:<12} {:>5} {:#018x} {frame_hash}\n",
row.frame, row.state
));
}
}
let path = golden_file();
std::fs::write(&path, text).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
eprintln!("blessed {}", path.display());
}
fn golden_file() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(GOLDEN_PATH)
}
fn parse_hash(text: &str) -> u64 {
let hex = text.strip_prefix("0x").unwrap_or(text);
u64::from_str_radix(hex, 16).unwrap_or_else(|e| panic!("`{text}` is not a hash: {e}"))
}
const GOLDEN_HEADER: &str = "\
# Frame and state hashes for the committed workloads (ROADMAP.md §12).
#
# Columns: workload, frames run, Machine::state_hash, Surface::hash — the last
# `-` for a machine this build has no scanout adapter for.
#
# These are generated. If a change to an emulated device moved one of them, that
# is the regression doing its job: confirm the new behaviour is the behaviour
# you meant, then re-record with
#
# RSEMU_BLESS_FRAME_HASHES=1 cargo test --all-features --test frame_hash
#
# and say in the commit message which device changed and why. A mismatch is
# never fixed by widening the test.
#
# The workloads themselves are generated too — see tests/workload/mod.rs. None
# of them is a commercial title, which is why the phase-3 fps gate is reported
# as unmet for licensing reasons rather than measured against one.
";