hg80 1.0.0

Z80 and Z80N CPU core, stepped one clock edge at a time
Documentation
//! The machine-cycle reports, checked against the reference simulation's own cycle boundaries.
//!
//! A host that works a bus cycle at a time takes this crate's word for where one cycle ends and
//! the next begins, and for how long each ran. If either is wrong, a peripheral clocked by the bus
//! advances at the wrong rate — which is a class of fault that shows up as software hanging rather
//! than as anything obviously miscounted.
//!
//! So the reports are compared against the trace: one report per machine cycle the reference runs,
//! carrying the address that cycle opened with, and a length that adds up to the T-states the
//! reference spent in it.
//!
//! The second test pins the relationship between the two per-cycle hooks. [`Host::wait_states`] is
//! asked as each cycle opens and [`Host::bus_cycle`] reports it once it has run, so between them a
//! host has both a before-notification and an after-notification for every machine cycle. A
//! consumer that must keep the phase it has — a peripheral clocked by the bus, where a one-cycle
//! shift has cost a hang before — can stay on the opening hook and take only the length from the
//! closing one. That is only true while the two describe the same cycles in the same order.
//!
//! The traces are carried here; the simulation that produced them is not. See `sim/t80n/run.sh`.

use hg80::{BusCycle, BusRequest, Cpu, Host};
use std::path::PathBuf;

struct Machine {
    memory: Vec<u8>,
    reports: Vec<(u16, u32)>,
    opened: Vec<BusRequest>,
    closed: Vec<BusRequest>,
    held: u32,
}

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,
    }
}

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.opened.push(*request);
        self.held
    }

    fn bus_cycle(&mut self, cycle: &BusCycle) {
        self.closed.push(cycle.request);
        self.reports
            .push((address_of(cycle.request), cycle.t_states));
    }
}

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
}

/// Where each machine cycle opens in the reference trace, and how many T-states it runs.
fn reference_cycles(text: &str) -> Vec<(u16, u32)> {
    let mut cycles: Vec<(u16, u32)> = Vec::new();
    let mut previous: Option<(u32, u32)> = None;

    for line in text.lines() {
        let fields: Vec<u32> = line
            .split_whitespace()
            .filter_map(|word| word.parse().ok())
            .collect();
        if fields.len() < 14 {
            continue;
        }
        let (machine_cycle, t_state, address) = (fields[0], fields[1], fields[2]);

        let opening = previous != Some((machine_cycle, t_state))
            && (previous.is_none_or(|(cycle, _)| cycle != machine_cycle) || t_state == 1);
        if opening && t_state == 1 {
            cycles.push((u16::try_from(address).unwrap_or_default(), 1));
        } else if let Some(last) = cycles.last_mut() {
            last.1 += 1;
        }
        previous = Some((machine_cycle, t_state));
    }
    cycles
}

/// The interrupt lines are raised a T-state later than the trace names them, for the reason set
/// out in `cycles.rs`: the reference drives them from a process on the falling edge.
fn signals(text: &str) -> (Option<usize>, Option<usize>) {
    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())
    };
    (field("int="), field("nmi="))
}

fn simulation() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sim/t80n")
}

fn run(
    image: &[u8],
    t_states: usize,
    held: u32,
    (interrupt_at, nonmaskable_at): (Option<usize>, Option<usize>),
) -> Machine {
    let mut cpu = Cpu::new();
    cpu.reset();
    cpu.set_z80n_enabled(true);
    let mut machine = Machine {
        memory: image.to_vec(),
        reports: Vec::new(),
        opened: Vec::new(),
        closed: Vec::new(),
        held,
    };
    for index in 0..t_states {
        if let Some(at) = interrupt_at {
            cpu.set_interrupt_requested(index > at);
        }
        if nonmaskable_at.is_some_and(|at| index == at + 1) {
            cpu.request_nmi();
        }
        cpu.tick(&mut machine);
        cpu.tick(&mut machine);
    }
    machine
}

#[test]
fn each_machine_cycle_is_reported_once_with_the_length_the_reference_spends_in_it() {
    let root = simulation();
    let programs = [
        "arithmetic",
        "loads",
        "blocks",
        "prefixed",
        "extended",
        "branches",
        "z80n_arith",
        "z80n_block",
        "z80n_wide",
        "halted",
    ];

    let mut failures = Vec::new();
    let mut compared = 0;

    for name in programs {
        for held in [0, 2] {
            let trace = if held == 0 {
                format!("traces/{name}.trace")
            } else {
                format!("traces/{name}.wait{held}.trace")
            };
            let Ok(text) = std::fs::read_to_string(root.join(&trace)) else {
                failures.push(format!("    {name}: no reference trace"));
                continue;
            };
            let image = std::fs::read_to_string(root.join(format!("programs/{name}.hex")))
                .unwrap_or_else(|_| panic!("{name}: no program image"));

            let want = reference_cycles(&text);
            let got = run(
                &parse_image(&image),
                text.lines().count(),
                held,
                signals(&image),
            )
            .reports;

            // The reference trace ends part way through a cycle and records it anyway; we do not
            // report a cycle until it has run. So the reference carries exactly one more than we
            // do, and everything we report is compared. Asserting the relation rather than taking
            // the shorter of the two is what stops a report going missing unnoticed.
            assert_eq!(
                want.len(),
                got.len() + 1,
                "{name} stalled {held}: {} reports against the reference's {}",
                got.len(),
                want.len()
            );
            let pairs = got.len();
            compared += pairs;

            for (index, (ours, theirs)) in got.iter().zip(want.iter()).take(pairs).enumerate() {
                if ours != theirs {
                    failures.push(format!(
                        "    {name} stalled {held}: cycle {index}: reported {:04x} for {} \
                         T-states, reference opens {:04x} and runs {}",
                        ours.0, ours.1, theirs.0, theirs.1
                    ));
                    break;
                }
            }
        }
    }

    println!("{compared} machine cycles compared against the reference");
    for failure in &failures {
        println!("{failure}");
    }

    assert!(failures.is_empty(), "{} reports diverged", failures.len());
    assert!(compared > 200, "only {compared} cycles were compared");
}

#[test]
fn the_opening_hook_and_the_closing_hook_describe_the_same_cycles_in_the_same_order() {
    let root = simulation();
    let mut compared = 0;

    for name in [
        "arithmetic",
        "prefixed",
        "z80n_block",
        "halted",
        "interrupt_mode2",
    ] {
        let Ok(text) = std::fs::read_to_string(root.join(format!("traces/{name}.trace"))) else {
            continue;
        };
        let image = std::fs::read_to_string(root.join(format!("programs/{name}.hex")))
            .unwrap_or_else(|_| panic!("{name}: no program image"));

        let seen = run(
            &parse_image(&image),
            text.lines().count(),
            0,
            signals(&image),
        );

        assert!(
            seen.opened.len() == seen.closed.len() || seen.opened.len() == seen.closed.len() + 1,
            "{name}: {} cycles opened against {} closed, which should differ by at most the one \
             still running",
            seen.opened.len(),
            seen.closed.len()
        );

        for (index, (opened, closed)) in seen.opened.iter().zip(seen.closed.iter()).enumerate() {
            assert_eq!(
                opened, closed,
                "{name}: cycle {index} opened as {opened:?} and closed as {closed:?}"
            );
        }
        compared += seen.closed.len();
    }

    println!("{compared} cycles matched between the opening and closing hooks");
    assert!(compared > 200, "only {compared} cycles were compared");
}