use hg80::{BusRequest, Cpu, Host, MachineCycle};
use std::path::PathBuf;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Kind {
Acknowledge,
Fetch,
Refresh,
Read,
Write,
PortRead,
PortWrite,
Internal,
}
impl Kind {
fn name(self) -> &'static str {
match self {
Self::Acknowledge => "acknowledge",
Self::Fetch => "fetch",
Self::Refresh => "refresh",
Self::Read => "read",
Self::Write => "write",
Self::PortRead => "port read",
Self::PortWrite => "port write",
Self::Internal => "internal",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Step {
machine_cycle: u8,
t_state: u8,
address: u16,
kind: Kind,
value: Option<u8>,
halted: bool,
interrupts_enabled: bool,
}
impl std::fmt::Display for Step {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"M{} T{} {:04x} {}",
self.machine_cycle,
self.t_state,
self.address,
self.kind.name()
)?;
if let Some(value) = self.value {
write!(formatter, " = {value:02x}")?;
}
if self.halted {
write!(formatter, " halted")?;
}
if self.interrupts_enabled {
write!(formatter, " enabled")?;
}
Ok(())
}
}
fn parse_reference(text: &str) -> Vec<Step> {
text.lines()
.filter_map(|line| {
let fields: Vec<u32> = line
.split_whitespace()
.filter_map(|word| word.parse().ok())
.collect();
if fields.len() < 14 {
return None;
}
let (m1, refresh, iorq, no_read, write, acknowledging) = (
fields[5], fields[6], fields[8], fields[9], fields[10], fields[11],
);
let kind = if m1 == 1 && acknowledging == 1 {
Kind::Acknowledge
} else if m1 == 1 {
Kind::Fetch
} else if refresh == 1 {
Kind::Refresh
} else if fields[0] == 1 {
Kind::Internal
} else if iorq == 1 {
if write == 1 {
Kind::PortWrite
} else {
Kind::PortRead
}
} else if write == 1 {
Kind::Write
} else if no_read == 1 {
Kind::Internal
} else {
Kind::Read
};
let t_state = u8::try_from(fields[1]).ok()?;
let transferring = matches!(kind, Kind::Write | Kind::PortWrite) && t_state == 3;
Some(Step {
machine_cycle: u8::try_from(fields[0]).ok()?,
t_state,
address: u16::try_from(fields[2]).ok()?,
kind,
value: transferring.then(|| u8::try_from(fields[4] & 0xFF).unwrap_or_default()),
halted: fields[7] == 1,
interrupts_enabled: fields.get(13) == Some(&1),
})
})
.collect()
}
fn parse_image(text: &str) -> Vec<u8> {
let mut memory = vec![0u8; 0x10000];
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut words = line.split_whitespace();
let Some(Ok(start)) = words.next().map(|word| u16::from_str_radix(word, 16)) else {
continue;
};
let mut at = start;
for word in words {
if let Ok(byte) = u8::from_str_radix(word, 16) {
memory[at as usize] = byte;
at = at.wrapping_add(1);
}
}
}
memory
}
struct Machine {
memory: Vec<u8>,
kind: Kind,
address: u16,
value: Option<u8>,
held: u32,
}
impl Host for Machine {
fn read(&mut self, address: u16, _at: u32) -> u8 {
self.memory[address as usize]
}
fn write(&mut self, address: u16, value: u8, _at: u32) {
self.memory[address as usize] = value;
}
fn input(&mut self, port: u16, _at: u32) -> u8 {
self.memory[port as usize]
}
fn output(&mut self, _port: u16, _value: u8, _at: u32) {}
fn wait_states(&mut self, _request: &BusRequest) -> u32 {
self.held
}
fn interrupt_vector(&mut self) -> u8 {
self.memory[self.address as usize]
}
fn bus_edge(&mut self, request: &BusRequest) {
let (kind, address, value) = match *request {
BusRequest::OpcodeFetch { address } => (Kind::Fetch, address, None),
BusRequest::Refresh { address } => (Kind::Refresh, address, None),
BusRequest::MemoryRead { address } => (Kind::Read, address, None),
BusRequest::MemoryWrite { address, value } => (Kind::Write, address, Some(value)),
BusRequest::PortRead { port } => (Kind::PortRead, port, None),
BusRequest::PortWrite { port, value } => (Kind::PortWrite, port, Some(value)),
BusRequest::Internal { address } => (Kind::Internal, address, None),
BusRequest::InterruptAcknowledge { address } => (Kind::Acknowledge, address, None),
_ => (Kind::Internal, 0, None),
};
self.kind = kind;
self.address = address;
self.value = value;
}
}
#[derive(Clone, Copy)]
struct Signals {
interrupt_at: Option<usize>,
nonmaskable_at: Option<usize>,
}
fn parse_header(text: &str) -> (usize, Signals) {
let header = text.lines().next().unwrap_or_default();
let field = |key: &str| {
header
.split_whitespace()
.find_map(|word| word.strip_prefix(key))
.and_then(|value| value.parse().ok())
};
let states = header
.split_whitespace()
.nth(1)
.and_then(|word| word.parse().ok())
.unwrap_or(200);
(
states,
Signals {
interrupt_at: field("int="),
nonmaskable_at: field("nmi="),
},
)
}
fn run_ours(image: &[u8], t_states: usize, held: u32, signals: Signals) -> Vec<Step> {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
let mut machine = Machine {
memory: image.to_vec(),
kind: Kind::Internal,
address: 0,
value: None,
held,
};
let mut steps = Vec::with_capacity(t_states);
for index in 0..t_states {
if let Some(at) = signals.interrupt_at {
cpu.set_interrupt_requested(index > at);
}
if signals.nonmaskable_at.is_some_and(|at| index == at + 1) {
cpu.request_nmi();
}
let machine_cycle = match cpu.machine_cycle() {
MachineCycle::M1 => 1,
MachineCycle::M2 => 2,
MachineCycle::M3 => 3,
MachineCycle::M4 => 4,
MachineCycle::M5 => 5,
MachineCycle::IndexDisplacement => 6,
MachineCycle::IndexAddition => 7,
};
let t_state = cpu.t_state();
let halted = cpu.is_halted();
let interrupts_enabled = cpu.registers().iff1;
cpu.tick(&mut machine);
cpu.tick(&mut machine);
steps.push(Step {
machine_cycle,
t_state,
address: machine.address,
kind: machine.kind,
value: (t_state == 3).then_some(machine.value).flatten(),
halted,
interrupts_enabled,
});
}
steps
}
fn simulation() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sim/t80n")
}
const UNCOMPARED_WRITES: [(&str, u16); 1] = [("z80n_block", 0x0156)];
fn compares_the_byte(name: &str, step: Step) -> bool {
!UNCOMPARED_WRITES
.iter()
.any(|&(program, address)| program == name && address == step.address)
}
fn compare(name: &str, held: u32) -> Result<(usize, usize, usize), String> {
let root = simulation();
let trace = if held == 0 {
format!("traces/{name}.trace")
} else {
format!("traces/{name}.wait{held}.trace")
};
let label = if held == 0 {
name.to_string()
} else {
format!("{name} stalled {held}")
};
let reference = std::fs::read_to_string(root.join(&trace))
.map_err(|_| format!("{label}: no reference trace"))?;
let image = std::fs::read_to_string(root.join(format!("programs/{name}.hex")))
.map_err(|_| format!("{label}: no program image"))?;
let want = parse_reference(&reference);
let (_, signals) = parse_header(&image);
let got = run_ours(&parse_image(&image), want.len(), held, signals);
assert_eq!(got.len(), want.len(), "{label}: trace lengths differ");
let mut differences = Vec::new();
let mut realigned = 0;
let mut exempted = 0;
let mut leading = 0;
let mut longest_lead = 0;
for (index, (ours, theirs)) in got.iter().zip(want.iter()).enumerate() {
let mut expected = *theirs;
if theirs.machine_cycle == 1 && theirs.t_state >= 3 {
expected.address = theirs.address.wrapping_add(1);
realigned += 1;
}
let mut ours = *ours;
if !compares_the_byte(name, *theirs) {
exempted += 1;
expected.value = None;
ours.value = None;
}
if ours.interrupts_enabled && !expected.interrupts_enabled {
leading += 1;
longest_lead = longest_lead.max(leading);
expected.interrupts_enabled = true;
} else {
leading = 0;
}
if ours != expected {
differences.push(format!(" T{index}: got {ours}, want {expected}"));
}
}
let allowed = 8 + 3 * held as usize;
if longest_lead > allowed {
differences.push(format!(
" the interrupt enable led the reference for {longest_lead} T-states, over {allowed}"
));
}
if realigned == 0 {
differences.push(" no refresh cycle was compared".to_string());
}
if differences.is_empty() {
Ok((want.len(), exempted, longest_lead))
} else {
let shown = differences.len();
differences.truncate(8);
Err(format!(
"{label}: {shown} of {} T-states differ\n{}",
want.len(),
differences.join("\n")
))
}
}
#[test]
fn the_core_matches_the_reference_simulation_t_state_for_t_state() {
let programs = [
"arithmetic",
"loads",
"blocks",
"prefixed",
"extended",
"interrupt_mode1",
"interrupt_mode2",
"nonmaskable",
"branches",
"z80n_arith",
"z80n_wide",
"z80n_block",
"z80n_block_match",
"z80n_ports",
"halted",
"interrupt_mode0",
"interrupt_delayed",
"interrupt_prefixed",
];
let mut failures = Vec::new();
let mut total = 0;
let mut exempted = 0;
let mut longest_lead = 0;
for name in programs {
for held in [0, 1, 2, 5] {
match compare(name, held) {
Ok((states, uncompared, lead)) => {
total += states;
exempted += uncompared;
longest_lead = longest_lead.max(lead);
}
Err(report) => failures.push(report),
}
}
}
if exempted == 0 {
failures.push(" no uncompared write occurred at any depth".to_string());
}
if longest_lead == 0 {
failures.push(" the interrupt enable never led, so it is no longer being read".into());
}
println!(
"{total} T-states compared across {} programs at four stall depths, {exempted} written \
bytes left uncompared, interrupt enable led by at most {longest_lead}",
programs.len()
);
for failure in &failures {
println!("{failure}");
}
assert!(failures.is_empty(), "{} programs diverged", failures.len());
}