#![cfg(all(feature = "cpu-riscv", feature = "cpu-mos6502"))]
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::thread::ThreadId;
use std::time::{Duration, Instant};
use rsemu::core::clock::{ClockForest, GlobalTime, Rational};
use rsemu::core::sched::{
AccessKind, Budget, Consumed, LazyDevice, LazyHandle, Runnable, Scheduler, SchedulerConfig,
ThreadingMode,
};
use rsemu::core::space::MemAttrs;
use rsemu::core::sync::Mutex;
use rsemu::core::value::Width;
use rsemu::machine::{Machine, catalog};
const HETEROGENEOUS: &str = include_str!("../machines/tests/heterogeneous.machine");
const RV_CODE: &[u32] = &[
0x0010_02b7, 0x0000_0313, 0x0013_0313, 0x1062_8023, 0x0002_c383, 0xfe03_8ae3, 0x0550_0e13, 0x11c2_8423, 0xfe9f_f06f, ];
const MOS_CODE: &[u8] = &[
0xa9, 0x00, 0x8d, 0x00, 0x40, 0x8d, 0x01, 0x40, 0x8d, 0x02, 0x40, 0xee, 0x00, 0x40, 0xd0, 0x03, 0xee, 0x01, 0x40, 0xad, 0x08, 0x41, 0xf0, 0xf3, 0xa9, 0xaa, 0x8d, 0x02, 0x40, 0x4c, 0x0b, 0xe0, ];
const RESET_VECTOR: usize = 0x1ffc;
fn mos_rom() -> Vec<u8> {
let mut rom = vec![0u8; 8 * 1024];
rom[..MOS_CODE.len()].copy_from_slice(MOS_CODE);
rom[RESET_VECTOR] = 0x00;
rom[RESET_VECTOR + 1] = 0xe0;
rom
}
fn rv_rom() -> Vec<u8> {
let mut rom = vec![0u8; 4 * 1024];
for (i, word) in RV_CODE.iter().enumerate() {
rom[i * 4..i * 4 + 4].copy_from_slice(&word.to_le_bytes());
}
rom
}
fn board(mode: ThreadingMode, workers: usize) -> Machine {
let mut options = catalog::build_options().expect("the catalog agrees with itself");
options.realize.scheduler.mode = mode;
options.realize.scheduler.workers = workers;
options.realize.media.insert("rvcode", rv_rom());
options.realize.media.insert("moscode", mos_rom());
let registry = catalog::registry().expect("a registry");
match rsemu::machine::build("heterogeneous.machine", HETEROGENEOUS, ®istry, &options) {
Ok(m) => m,
Err(e) => panic!("the fixture does not realize: {e}"),
}
}
fn shared(m: &Machine, offset: u64) -> u64 {
m.space("big")
.expect("the big-endian space")
.read(0x4000 + offset, Width::U8, MemAttrs::DEFAULT)
.expect("a mapped byte")
}
#[derive(Debug)]
struct Rendezvous {
want: usize,
seen: Arc<AtomicUsize>,
threads: Arc<Mutex<Vec<ThreadId>>>,
all_here: Arc<AtomicUsize>,
}
impl Runnable for Rendezvous {
fn run(&mut self, budget: Budget) -> Consumed {
self.threads.lock().push(std::thread::current().id());
self.seen.fetch_add(1, Ordering::AcqRel);
let deadline = Instant::now() + Duration::from_secs(5);
while self.seen.load(Ordering::Acquire) < self.want && Instant::now() < deadline {
std::hint::spin_loop();
}
if self.seen.load(Ordering::Acquire) >= self.want {
self.all_here.fetch_add(1, Ordering::AcqRel);
}
Consumed::new(budget.ticks)
}
}
#[test]
fn every_runnable_is_inside_its_run_call_on_a_thread_of_its_own() {
const CORES: usize = 3;
let mut forest = ClockForest::new();
let mut domains = Vec::new();
for i in 0..CORES {
let osc = forest
.add_oscillator(&format!("xtal{i}"), Rational::integer(1_000_000))
.unwrap();
domains.push(forest.add_domain(&format!("core{i}"), osc, 1, 1).unwrap());
}
let seen = Arc::new(AtomicUsize::new(0));
let threads = Arc::new(Mutex::new(Vec::new()));
let all_here = Arc::new(AtomicUsize::new(0));
let mut sched = Scheduler::new(
forest,
SchedulerConfig {
mode: ThreadingMode::Parallel,
workers: CORES,
..SchedulerConfig::default()
},
);
assert_eq!(
sched.pool().expect("parallel mode builds a pool").workers(),
CORES,
"this host refused a worker thread; the rest of the test is meaningless without it"
);
for domain in domains {
sched.add_runnable(
domain,
Box::new(Rendezvous {
want: CORES,
seen: Arc::clone(&seen),
threads: Arc::clone(&threads),
all_here: Arc::clone(&all_here),
}),
);
}
sched.run_quantum().expect("a parallel round");
let ids = threads.lock().clone();
assert_eq!(
all_here.load(Ordering::Acquire),
CORES,
"the runnables never all overlapped: the round serialised them"
);
assert_eq!(ids.len(), CORES, "every runnable ran");
for (i, a) in ids.iter().enumerate() {
for b in &ids[i + 1..] {
assert_ne!(a, b, "two runnables shared a host thread");
}
}
let here = std::thread::current().id();
assert_eq!(
ids.iter().filter(|id| **id == here).count(),
1,
"exactly one runnable stays on the thread that drives the round; the rest are dispatched"
);
}
#[derive(Debug)]
struct SlowDevice {
tick: u64,
advances: Arc<AtomicU64>,
}
impl LazyDevice for SlowDevice {
fn current_tick(&self) -> u64 {
self.tick
}
fn advance_to(&mut self, tick: u64) {
let mut sum = 0u64;
for i in 0..2_000u64 {
sum = sum.wrapping_add(i);
}
std::hint::black_box(sum);
self.tick = tick;
self.advances.fetch_add(1, Ordering::Relaxed);
}
}
#[derive(Debug)]
struct Sampler {
handle: LazyHandle,
errors: Arc<AtomicU64>,
}
impl Runnable for Sampler {
fn run(&mut self, budget: Budget) -> Consumed {
for _ in 0..64 {
if self.handle.sync(AccessKind::Guest).is_err() {
self.errors.fetch_add(1, Ordering::Relaxed);
}
}
Consumed::new(budget.ticks)
}
}
#[test]
fn two_cpus_reaching_one_lazy_device_is_contention_not_re_entrancy() {
let mut forest = ClockForest::new();
let a = forest
.add_oscillator("a", Rational::integer(1_000_000))
.unwrap();
let b = forest
.add_oscillator("b", Rational::integer(1_000_000))
.unwrap();
let da = forest.add_domain("da", a, 1, 1).unwrap();
let db = forest.add_domain("db", b, 1, 1).unwrap();
let mut sched = Scheduler::new(
forest,
SchedulerConfig {
mode: ThreadingMode::Parallel,
workers: 2,
..SchedulerConfig::default()
},
);
let advances = Arc::new(AtomicU64::new(0));
let dev = sched.add_lazy_device(
da,
Box::new(SlowDevice {
tick: 0,
advances: Arc::clone(&advances),
}),
);
let handle = sched.lazy_handle(dev).unwrap();
let errors = Arc::new(AtomicU64::new(0));
for domain in [da, db] {
sched.add_runnable(
domain,
Box::new(Sampler {
handle: handle.clone(),
errors: Arc::clone(&errors),
}),
);
}
for _ in 0..40 {
sched.run_quantum().expect("a parallel round");
}
assert_eq!(
errors.load(Ordering::Relaxed),
0,
"a catch-up that found the slot taken by the *other thread* reported re-entrancy"
);
assert!(advances.load(Ordering::Relaxed) > 0, "the device advanced");
}
fn run_until_both_saw_each_other(m: &mut Machine) -> GlobalTime {
let start = m.now();
for _ in 0..200 {
m.run_for(GlobalTime::from_nanos(1_000_000))
.expect("a round");
if shared(m, 0x108) == 0x55 && shared(m, 2) == 0xaa {
return m.now().saturating_sub(start);
}
}
panic!(
"after 200 ms of virtual time: 6502 counter {:#04x}{:#04x}, riscv flag {:#04x}, \
6502 flag {:#04x}",
shared(m, 1),
shared(m, 0),
shared(m, 0x108),
shared(m, 2)
);
}
#[test]
fn the_heterogeneous_board_runs_both_cpus_in_parallel() {
let mut m = board(ThreadingMode::Parallel, 2);
m.reset(rsemu::core::device::ResetKind::Cold);
run_until_both_saw_each_other(&mut m);
assert_eq!(shared(&m, 0x108), 0x55, "the hart saw the 6502");
assert_eq!(shared(&m, 2), 0xaa, "the 6502 saw the hart");
let counter = shared(&m, 0) | (shared(&m, 1) << 8);
assert!(counter > 0, "the 6502's own counter moved");
assert!(
m.now() > GlobalTime::ZERO,
"virtual time moved: {:?}",
m.now()
);
}
fn tick_counts(m: &Machine) -> Vec<u64> {
m.devices()
.iter()
.filter(|d| d.runnable().is_some())
.filter_map(|d| d.domain())
.map(|domain| m.clocks().ticks(domain).expect("a tick count"))
.collect()
}
#[test]
fn a_real_core_unwinds_at_its_next_instruction_when_the_flag_is_raised() {
let mut m = board(ThreadingMode::Parallel, 2);
m.reset(rsemu::core::device::ResetKind::Cold);
m.run_for(GlobalTime::from_nanos(5_000_000)).expect("a run");
let free = tick_counts(&m);
m.run_for(GlobalTime::from_nanos(1_000_000))
.expect("a round");
let after_free = tick_counts(&m);
let guard = m.stop_the_world();
let stopped = tick_counts(&m);
m.run_for(GlobalTime::from_nanos(1_000_000))
.expect("a round under the guard");
let after_stopped = tick_counts(&m);
drop(guard);
for (i, (&before_free, &before_stopped)) in free.iter().zip(&stopped).enumerate() {
let ran_free = after_free[i] - before_free;
let ran_stopped = after_stopped[i] - before_stopped;
assert!(ran_free > 0, "runnable {i} did nothing to compare against");
assert!(
ran_stopped * 4 < ran_free,
"runnable {i} executed {ran_stopped} ticks under a world stop against {ran_free} \
without one; it is not consulting its exit flag"
);
}
}
#[test]
fn the_same_board_runs_the_same_way_in_deterministic_mode() {
let mut m = board(ThreadingMode::Deterministic, 0);
m.reset(rsemu::core::device::ResetKind::Cold);
run_until_both_saw_each_other(&mut m);
assert_eq!(shared(&m, 0x108), 0x55);
assert_eq!(shared(&m, 2), 0xaa);
}
#[test]
fn deterministic_mode_is_still_bit_reproducible_on_this_board() {
let mut hashes = Vec::new();
for _ in 0..3 {
let mut m = board(ThreadingMode::Deterministic, 0);
m.reset(rsemu::core::device::ResetKind::Cold);
m.run_for(GlobalTime::from_nanos(20_000_000))
.expect("a run");
hashes.push(m.state_hash().expect("deterministic mode hashes"));
}
assert_eq!(hashes[0], hashes[1]);
assert_eq!(hashes[1], hashes[2]);
}
#[test]
fn a_state_hash_is_refused_outside_a_deterministic_mode() {
let m = board(ThreadingMode::Parallel, 2);
let err = m
.state_hash()
.expect_err("a parallel state hash is a sample, not a baseline");
let text = err.to_string();
assert!(text.contains("parallel"), "{text}");
assert!(text.contains("reproducible"), "{text}");
assert!(m.nondeterministic_state_hash().is_ok());
}
#[test]
fn a_snapshot_taken_with_the_world_stopped_restores_and_continues() {
let mut m = board(ThreadingMode::Parallel, 2);
m.reset(rsemu::core::device::ResetKind::Cold);
run_until_both_saw_each_other(&mut m);
let (bytes, before) = {
let guard = m.stop_the_world();
assert!(guard.generation() >= 1);
assert!(m.safe_point().stop_requested());
(
m.save().expect("a snapshot"),
m.nondeterministic_state_hash().expect("a hash"),
)
};
assert!(
!m.safe_point().stop_requested(),
"the guard let the world go"
);
let mut restored = board(ThreadingMode::Parallel, 2);
restored.load(&bytes).expect("the snapshot loads");
assert_eq!(
restored.nondeterministic_state_hash().expect("a hash"),
before,
"a restored machine is the machine that was saved"
);
let counter_before = shared(&restored, 0) | (shared(&restored, 1) << 8);
restored
.run_for(GlobalTime::from_nanos(20_000_000))
.expect("a run after restore");
let counter_after = shared(&restored, 0) | (shared(&restored, 1) << 8);
assert_ne!(
counter_after, counter_before,
"the restored machine executed"
);
}
#[test]
fn the_lock_ladder_holds_with_two_cpus_inside_it_at_once() {
for _ in 0..8 {
let mut m = board(ThreadingMode::Parallel, 2);
m.reset(rsemu::core::device::ResetKind::Cold);
run_until_both_saw_each_other(&mut m);
}
}
#[test]
fn a_parallel_machine_with_no_workers_still_runs() {
let mut m = board(ThreadingMode::Parallel, 0);
assert_eq!(
m.scheduler().pool().expect("a pool").workers(),
0,
"no workers were asked for and none were made"
);
m.reset(rsemu::core::device::ResetKind::Cold);
run_until_both_saw_each_other(&mut m);
assert_eq!(shared(&m, 0x108), 0x55);
assert_eq!(shared(&m, 2), 0xaa);
}
#[cfg(feature = "machine-apple1")]
mod wired {
use super::*;
fn apple1(mode: ThreadingMode, workers: usize) -> Machine {
let entry = catalog::machine("apple1").expect("this build ships apple1");
let mut options = catalog::build_options().expect("the catalog agrees with itself");
options.realize.scheduler.mode = mode;
options.realize.scheduler.workers = workers;
options
.realize
.media
.insert("rom", rsemu::dev::apple1::RSMON.as_slice());
let registry = catalog::registry().expect("a registry");
rsemu::machine::build(entry.name, entry.source, ®istry, &options)
.expect("the board realizes")
}
#[test]
fn a_wire_between_two_runnables_survives_two_threads() {
for _ in 0..12 {
let mut m = apple1(ThreadingMode::Parallel, 2);
m.reset(rsemu::core::device::ResetKind::Cold);
m.run_for(GlobalTime::from_nanos(20_000_000))
.expect("a parallel run");
let cpu = m
.device("cpu")
.expect("the core")
.domain()
.expect("a domain");
assert!(
m.clocks().ticks(cpu).expect("a tick count") > 0,
"the 6502 executed"
);
}
}
#[test]
fn the_same_board_is_still_bit_identical_in_deterministic_mode() {
let mut hashes = Vec::new();
for _ in 0..3 {
let mut m = apple1(ThreadingMode::Deterministic, 0);
m.reset(rsemu::core::device::ResetKind::Cold);
m.run_for(GlobalTime::from_nanos(200_000_000))
.expect("a run");
hashes.push(m.state_hash().expect("deterministic mode hashes"));
}
assert_eq!(hashes[0], hashes[1]);
assert_eq!(hashes[1], hashes[2]);
}
}