use hg80::{BusCycle, BusRequest, Cpu, Host, Registers};
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn byte(&mut self) -> u8 {
self.next().to_le_bytes()[3]
}
fn pick<'a, T>(&mut self, items: &'a [T]) -> &'a T {
&items[usize::from(self.byte()) % items.len()]
}
}
const POOL: &[&[u8]] = &[
&[0x00], &[0x3E, 0xFF], &[0x06, 0xFF], &[0x47], &[0x7E], &[0x77], &[0x36, 0xFF], &[0x21, 0xFF, 0x40], &[0x11, 0xFF, 0x40], &[0x01, 0xFF, 0x00], &[0xC6, 0xFF], &[0xCE, 0xFF], &[0xD6, 0xFF], &[0xE6, 0xFF], &[0xB6], &[0xFE, 0xFF], &[0x3C], &[0x35], &[0x27], &[0x2F], &[0x37], &[0x3F], &[0x07], &[0x1F], &[0x23], &[0x2B], &[0x09], &[0xE5], &[0xE1], &[0xF5], &[0xF1], &[0xEB], &[0x08], &[0xD9], &[0xE3], &[0xCB, 0x27], &[0xCB, 0x1E], &[0xCB, 0x46], &[0xCB, 0xC6], &[0xDD, 0x7E, 0x02], &[0xDD, 0x77, 0x03], &[0xFD, 0x35, 0x01], &[0xDD, 0xCB, 0x02, 0x46], &[0xDD, 0x23], &[0xED, 0x44], &[0xED, 0x4A], &[0xED, 0x42], &[0xED, 0x67], &[0xED, 0x6F], &[0xED, 0x57], &[0xED, 0x5F], &[0xED, 0xA0], &[0xED, 0xA8], &[0xED, 0xA1], &[0xDB, 0xFE], &[0xD3, 0xFE], &[0xED, 0x78], &[0xED, 0x79], &[0x18, 0x00], &[0x20, 0x00], &[0x10, 0x00], &[0xFB], &[0xF3], &[0xED, 0x56], &[0xED, 0x5E], &[0xED, 0x4D], &[0xED, 0x45], &[0x76], &[0xED, 0x30], &[0xED, 0x31], &[0xED, 0x23], &[0xED, 0x24], &[0xED, 0x28], &[0xED, 0x91, 0x07, 0xFF], &[0xED, 0x92, 0x07], &[0xED, 0x94], &[0xED, 0xA4], &[0xED, 0x90], ];
const ORIGIN: u16 = 0x8000;
const SCRATCH: u16 = 0x4000;
fn program(rng: &mut Rng, count: usize) -> Vec<u8> {
let mut bytes = Vec::new();
for _ in 0..count {
let shape: &[u8] = rng.pick(POOL);
for &byte in shape {
bytes.push(if byte == 0xFF { rng.byte() } else { byte });
}
}
bytes.extend_from_slice(&[0xC3, ORIGIN.to_le_bytes()[0], ORIGIN.to_le_bytes()[1]]);
bytes
}
#[derive(Clone, PartialEq, Eq, Debug)]
enum Event {
Opening { kind: &'static str, address: u16 },
Closed { kind: &'static str, t_states: u32 },
Fetch { address: u16, at: u32, value: u8 },
Read { address: u16, at: u32, value: u8 },
Write { address: u16, at: u32, value: u8 },
Input { port: u16, at: u32, value: u8 },
Output { port: u16, at: u32, value: u8 },
Vector { value: u8 },
ReturnFromInterrupt,
Extended { command: &'static str, data: u16 },
}
fn kind_of(request: BusRequest) -> &'static str {
match request {
BusRequest::OpcodeFetch { .. } => "fetch",
BusRequest::MemoryRead { .. } => "read",
BusRequest::MemoryWrite { .. } => "write",
BusRequest::PortRead { .. } => "in",
BusRequest::PortWrite { .. } => "out",
BusRequest::Refresh { .. } => "refresh",
BusRequest::Internal { .. } => "internal",
BusRequest::InterruptAcknowledge { .. } => "ack",
_ => "other",
}
}
fn address_of(request: BusRequest) -> u16 {
match request {
BusRequest::OpcodeFetch { address }
| BusRequest::MemoryRead { address }
| BusRequest::MemoryWrite { address, .. }
| BusRequest::Refresh { address }
| BusRequest::Internal { address }
| BusRequest::InterruptAcknowledge { address } => address,
BusRequest::PortRead { port } | BusRequest::PortWrite { port, .. } => port,
_ => 0,
}
}
struct Recorder {
bytes: Box<[u8; 0x10000]>,
events: Vec<Event>,
waits: u32,
port_seed: u8,
}
impl Host for Recorder {
fn read(&mut self, address: u16, at: u32) -> u8 {
let value = self.bytes[address as usize];
self.events.push(Event::Read { address, at, value });
value
}
fn fetch(&mut self, address: u16, at: u32) -> u8 {
let value = self.bytes[address as usize];
self.events.push(Event::Fetch { address, at, value });
value
}
fn write(&mut self, address: u16, value: u8, at: u32) {
self.bytes[address as usize] = value;
self.events.push(Event::Write { address, at, value });
}
fn input(&mut self, port: u16, at: u32) -> u8 {
let [high, low] = port.to_be_bytes();
let value = self.port_seed ^ low ^ high;
self.events.push(Event::Input { port, at, value });
value
}
fn output(&mut self, port: u16, value: u8, at: u32) {
self.events.push(Event::Output { port, at, value });
}
fn wait_states(&mut self, request: &BusRequest) -> u32 {
self.events.push(Event::Opening {
kind: kind_of(*request),
address: address_of(*request),
});
self.waits
}
fn bus_cycle(&mut self, cycle: &BusCycle) {
self.events.push(Event::Closed {
kind: kind_of(cycle.request),
t_states: cycle.t_states,
});
}
fn interrupt_vector(&mut self) -> u8 {
self.events.push(Event::Vector { value: 0xFF });
0xFF
}
fn return_from_interrupt(&mut self) {
self.events.push(Event::ReturnFromInterrupt);
}
fn z80n_command(&mut self, command: hg80::Z80nCommand, data: u16) {
let _ = command;
self.events.push(Event::Extended {
command: "extended",
data,
});
}
}
struct Trace {
events: Vec<Event>,
registers: Registers,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Drive {
Instruction,
Cycle,
Edge,
}
fn run(seed: u64, waits: u32, t_states: u32) -> Trace {
driven(seed, waits, t_states, Drive::Instruction)
}
fn driven(seed: u64, waits: u32, t_states: u32, how: Drive) -> Trace {
let mut rng = Rng(seed | 1);
let code = program(&mut rng, 24);
let mut bytes = vec![0u8; 0x10000].into_boxed_slice();
bytes[ORIGIN as usize..ORIGIN as usize + code.len()].copy_from_slice(&code);
for offset in 0..0x0100usize {
bytes[SCRATCH as usize + offset] = rng.byte();
}
let mut host = Recorder {
bytes: bytes.try_into().unwrap(),
events: Vec::new(),
waits,
port_seed: rng.byte(),
};
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(seed & 2 != 0);
let registers = cpu.registers_mut();
registers.af = u16::from(rng.byte()) << 8 | u16::from(rng.byte());
registers.bc = SCRATCH | u16::from(rng.byte() & 0x3F);
registers.de = SCRATCH | u16::from(rng.byte() & 0x3F);
registers.hl = SCRATCH | u16::from(rng.byte() & 0x3F);
registers.ix = SCRATCH | u16::from(rng.byte() & 0x3F);
registers.iy = SCRATCH | u16::from(rng.byte() & 0x3F);
registers.sp = SCRATCH + 0x0080;
registers.i = rng.byte();
registers.iff1 = seed & 8 != 0;
registers.iff2 = registers.iff1;
registers.pc = ORIGIN;
cpu.abandon_instruction();
cpu.set_interrupt_requested(seed & 4 != 0);
if seed & 16 != 0 {
cpu.request_nmi();
}
let mut total = 0;
while total < t_states {
total += match how {
Drive::Instruction => cpu.step(&mut host),
Drive::Cycle => cpu.run_cycle(&mut host),
Drive::Edge => {
cpu.tick(&mut host);
cpu.tick(&mut host);
1
}
};
}
Trace {
events: host.events,
registers: *cpu.registers(),
}
}
fn prefix_differs(left: &Trace, right: &Trace) -> Option<String> {
for (index, (a, b)) in left.events.iter().zip(right.events.iter()).enumerate() {
if a != b {
return Some(format!(
"event {index} differs:\n left {a:?}\n right {b:?}"
));
}
}
let shared = left.events.len().min(right.events.len());
assert!(
shared > 500,
"only {shared} events in common, nothing was compared"
);
let lead = left.events.len().checked_sub(right.events.len());
assert!(
lead.is_some_and(|lead| (1..=24).contains(&lead)),
"the instruction-driven run has {} events against {}, outside the measured lead of one to \
twelve",
left.events.len(),
right.events.len()
);
None
}
fn difference(left: &Trace, right: &Trace) -> Option<String> {
for (index, (a, b)) in left.events.iter().zip(right.events.iter()).enumerate() {
if a != b {
return Some(format!(
"event {index} differs:\n left {a:?}\n right {b:?}"
));
}
}
if left.events.len() != right.events.len() {
let shared = left.events.len().min(right.events.len());
let (name, longer) = if left.events.len() > right.events.len() {
("left", left)
} else {
("right", right)
};
return Some(format!(
"{name} has {} events, the other has {shared}; its first extra is {:?}",
longer.events.len(),
longer.events[shared]
));
}
if left.registers != right.registers {
return Some(format!(
"registers differ:\n left {:?}\n right {:?}",
left.registers, right.registers
));
}
None
}
const PROGRAMS: u64 = 200;
const BUDGET: u32 = 4_000;
#[test]
fn each_program_is_reproducible_from_its_seed() {
for seed in 0..PROGRAMS {
for waits in [0u32, 1, 3] {
let left = run(seed, waits, BUDGET);
let right = run(seed, waits, BUDGET);
assert!(
difference(&left, &right).is_none(),
"seed {seed} at {waits} waits: {}",
difference(&left, &right).unwrap()
);
}
}
}
#[test]
fn the_programs_exercise_every_axis_the_comparison_watches() {
let mut kinds = std::collections::BTreeSet::new();
let mut offsets = std::collections::BTreeSet::new();
let mut events = 0usize;
for seed in 0..PROGRAMS {
let trace = run(seed, 1, BUDGET);
events += trace.events.len();
for event in &trace.events {
match event {
Event::Opening { kind, .. } => {
kinds.insert(*kind);
}
Event::Fetch { at, .. }
| Event::Read { at, .. }
| Event::Write { at, .. }
| Event::Input { at, .. }
| Event::Output { at, .. } => {
offsets.insert(*at);
}
_ => {}
}
}
}
for wanted in ["fetch", "read", "write", "in", "out", "internal", "ack"] {
assert!(
kinds.contains(wanted),
"no generated program opened a {wanted} cycle"
);
}
assert!(
offsets.len() >= 3,
"the generated programs saw only {offsets:?} as transfer offsets"
);
assert!(
events > 200_000,
"the generated programs are only {events} events"
);
println!(
"{PROGRAMS} programs, {events} events, cycle kinds {kinds:?}, offsets {offsets:?}"
);
}
#[test]
fn the_comparison_catches_every_kind_of_divergence() {
let reference = run(7, 1, BUDGET);
let index_of = |pick: fn(&Event) -> bool| {
reference
.events
.iter()
.position(pick)
.expect("the generated program contains one of these")
};
let planted: Vec<(&str, Trace)> = vec![
("a value", {
let mut t = clone_of(&reference);
let at = index_of(|e| matches!(e, Event::Read { .. }));
if let Event::Read { value, .. } = &mut t.events[at] {
*value ^= 0x01;
}
t
}),
("an offset", {
let mut t = clone_of(&reference);
let at = index_of(|e| matches!(e, Event::Write { .. }));
if let Event::Write { at: offset, .. } = &mut t.events[at] {
*offset += 1;
}
t
}),
("an address", {
let mut t = clone_of(&reference);
let at = index_of(|e| matches!(e, Event::Write { .. }));
if let Event::Write { address, .. } = &mut t.events[at] {
*address = address.wrapping_add(1);
}
t
}),
("the order", {
let mut t = clone_of(&reference);
let at = index_of(|e| matches!(e, Event::Read { .. }));
t.events.swap(at, at + 1);
t
}),
("a call count", {
let mut t = clone_of(&reference);
let at = index_of(|e| matches!(e, Event::Input { .. } | Event::Read { .. }));
let duplicate = t.events[at].clone();
t.events.insert(at, duplicate);
t
}),
("a missing call", {
let mut t = clone_of(&reference);
let at = index_of(|e| matches!(e, Event::Read { .. }));
t.events.remove(at);
t
}),
("a cycle length", {
let mut t = clone_of(&reference);
let at = index_of(|e| matches!(e, Event::Closed { .. }));
if let Event::Closed { t_states, .. } = &mut t.events[at] {
*t_states += 1;
}
t
}),
("a register", {
let mut t = clone_of(&reference);
t.registers.hl = t.registers.hl.wrapping_add(1);
t
}),
("the flags", {
let mut t = clone_of(&reference);
t.registers.af ^= 0x0001;
t
}),
("the memory pointer", {
let mut t = clone_of(&reference);
t.registers.wz = t.registers.wz.wrapping_add(1);
t
}),
];
for (axis, altered) in planted {
assert!(
difference(&reference, &altered).is_some(),
"the comparison did not catch a planted difference in {axis}"
);
}
}
fn clone_of(trace: &Trace) -> Trace {
Trace {
events: trace.events.clone(),
registers: trace.registers,
}
}
#[test]
fn every_way_of_driving_the_core_produces_the_same_run() {
for seed in 0..PROGRAMS {
for waits in [0u32, 1, 3] {
let by_instruction = driven(seed, waits, BUDGET, Drive::Instruction);
for how in [Drive::Cycle, Drive::Edge] {
let other = driven(seed, waits, BUDGET, how);
assert!(
prefix_differs(&by_instruction, &other).is_none(),
"seed {seed} at {waits} waits, {how:?} against Instruction: {}",
prefix_differs(&by_instruction, &other).unwrap()
);
}
}
}
}
#[test]
fn run_cycle_returns_the_length_the_cycle_reports() {
let trace = driven(11, 2, BUDGET, Drive::Cycle);
let closed: Vec<u32> = trace
.events
.iter()
.filter_map(|event| match event {
Event::Closed { t_states, .. } => Some(*t_states),
_ => None,
})
.collect();
assert!(closed.len() > 100, "only {} cycles closed", closed.len());
assert!(
closed.iter().all(|&t| t >= 3),
"a machine cycle shorter than three T-states: {closed:?}"
);
}