hg80 1.0.0

Z80 and Z80N CPU core, stepped one clock edge at a time
Documentation
//! Saving and restoring the core part-way through an instruction.
//!
//! The register set is only the architectural part of what a processor holds. Between two clock
//! edges there's also a decoded opcode, a position in the machine cycle, a latched byte, and the
//! pipeline registers the next edge will commit. Save a core mid-instruction and restore it, and it
//! has to carry all of that, or it comes back as a different processor.
//!
//! This runs a program to every half-T-state in turn, round-trips the core at each one, and makes
//! the restored core both compare equal and produce the same remaining trace.

#![cfg(feature = "serde")]

use hg80::{BusRequest, Cpu, Host};

struct Machine {
    bytes: Vec<u8>,
    trace: Vec<(u8, u16)>,
}

impl Machine {
    fn with(program: &[u8]) -> Self {
        let mut bytes = vec![0u8; 0x10000];
        bytes[..program.len()].copy_from_slice(program);
        bytes[0x40] = 0x5A;
        bytes[0x41] = 0xC3;
        Self {
            bytes,
            trace: Vec::new(),
        }
    }
}

impl Host for Machine {
    fn read(&mut self, address: u16, _at: u32) -> u8 {
        self.bytes[address as usize]
    }

    fn write(&mut self, address: u16, value: u8, _at: u32) {
        self.bytes[address as usize] = value;
    }

    fn input(&mut self, _port: u16, _at: u32) -> u8 {
        0x7E
    }

    fn output(&mut self, _port: u16, _value: u8, _at: u32) {}

    fn bus_edge(&mut self, request: &BusRequest) {
        let entry = match *request {
            BusRequest::OpcodeFetch { address } => (1, address),
            BusRequest::Refresh { address } => (2, address),
            BusRequest::MemoryRead { address } => (3, address),
            BusRequest::MemoryWrite { address, value } => (4, address ^ u16::from(value)),
            BusRequest::PortRead { port } => (5, port),
            BusRequest::PortWrite { port, .. } => (6, port),
            BusRequest::Internal { address } => (7, address),
            BusRequest::InterruptAcknowledge { address } => (8, address),
            _ => (9, 0),
        };
        self.trace.push(entry);
    }
}

const PROGRAM: &[u8] = &[
    0x21, 0x40, 0x00, // LD HL, 0x0040
    0x11, 0x80, 0x00, // LD DE, 0x0080
    0x01, 0x02, 0x00, // LD BC, 0x0002
    0xED, 0xB0, // LDIR
    0xDD, 0x21, 0x50, 0x00, // LD IX, 0x0050
    0xDD, 0xCB, 0x02, 0x46, // BIT 0, (IX+2)
    0x3E, 0x99, // LD A, 0x99
    0xCD, 0x20, 0x00, // CALL 0x0020
];

struct Flat {
    bytes: Vec<u8>,
}

impl Flat {
    fn with(program: &[u8]) -> Self {
        let mut bytes = vec![0u8; 0x10000];
        bytes[..program.len()].copy_from_slice(program);
        Self { bytes }
    }
}

impl Host for Flat {
    fn read(&mut self, address: u16, _at: u32) -> u8 {
        self.bytes[address as usize]
    }
    fn write(&mut self, address: u16, value: u8, _at: u32) {
        self.bytes[address as usize] = value;
    }
    fn input(&mut self, _port: u16, _at: u32) -> u8 {
        0xFF
    }
    fn output(&mut self, _port: u16, _value: u8, _at: u32) {}
}

fn run(cpu: &mut Cpu, machine: &mut Machine, edges: usize) {
    for _ in 0..edges {
        cpu.tick(machine);
    }
}

fn fresh() -> (Cpu, Machine) {
    let mut cpu = Cpu::new();
    cpu.reset();
    cpu.set_z80n_enabled(true);
    cpu.registers_mut().sp = 0x0200;
    (cpu, Machine::with(PROGRAM))
}

#[test]
fn a_core_paused_between_any_two_edges_resumes_the_same_after_a_round_trip() {
    const EDGES: usize = 260;

    for pause in 0..EDGES {
        let (mut cpu, mut machine) = fresh();
        run(&mut cpu, &mut machine, pause);

        let carried = serde_json::to_string(&cpu).expect("the core serialises");
        let mut restored: Cpu = serde_json::from_str(&carried).expect("the core deserialises");

        assert_eq!(cpu, restored, "paused at edge {pause}");

        let mark = machine.trace.len();
        let mut continued = Machine {
            bytes: machine.bytes.clone(),
            trace: Vec::new(),
        };
        run(&mut cpu, &mut machine, EDGES - pause);
        run(&mut restored, &mut continued, EDGES - pause);

        assert_eq!(
            machine.trace[mark..],
            continued.trace[..],
            "the trace after edge {pause} differs"
        );
        assert_eq!(cpu, restored, "the cores diverged after edge {pause}");
        assert_eq!(machine.bytes, continued.bytes, "memory differs at {pause}");
    }
}

#[test]
fn a_core_paused_mid_instruction_is_not_merely_its_register_set() {
    let (mut cpu, mut machine) = fresh();
    run(&mut cpu, &mut machine, 79);

    let mut rebuilt = Cpu::new();
    rebuilt.reset();
    rebuilt.set_z80n_enabled(true);
    *rebuilt.registers_mut() = *cpu.registers();

    assert_eq!(cpu.registers(), rebuilt.registers());
    assert_ne!(
        cpu, rebuilt,
        "a core carrying only its registers should not compare equal part-way through an \
         instruction"
    );
}

// Saving and restoring between instructions, which is what a machine does. The second program is
// here because the first never halts, never enables interrupts and never writes an extended
// register, so it leaves untouched most of what outlives a step.
//
// What this cannot do is prove the engine hands everything back: dropping a field from the
// hand-over leaves it stale in the processor, which is then handed *in* again, so both sides of the
// comparison go wrong together and agree. Measured — three of the four handed-over values are
// caught by the behavioural suites instead, and the fourth is the address bus, which a step always
// overwrites with its opening fetch before anything reads it.
const REACHES_THE_LATCHES: &[u8] = &{
    let mut bytes = [0u8; 0x40];
    bytes[0] = 0xED;
    bytes[1] = 0x91;
    bytes[2] = 0x07;
    bytes[3] = 0x02;
    bytes[4] = 0xFB;
    bytes[6] = 0x76;
    bytes[0x38] = 0xC9;
    bytes
};

#[test]
fn a_core_paused_between_any_two_instructions_resumes_the_same_after_a_round_trip() {
    for program in [PROGRAM, REACHES_THE_LATCHES] {
        paused_at_every_instruction(program);
    }
}

fn paused_at_every_instruction(program: &[u8]) {
    const STEPS: usize = 60;

    for pause in 0..STEPS {
        let mut cpu = Cpu::new();
        cpu.reset();
        cpu.set_z80n_enabled(true);
        cpu.registers_mut().sp = 0x0200;
        cpu.set_interrupt_requested(true);
        let mut machine = Machine::with(program);
        for _ in 0..pause {
            cpu.step(&mut machine);
        }

        let carried = serde_json::to_string(&cpu).expect("the core serialises");
        let mut restored: Cpu = serde_json::from_str(&carried).expect("the core deserialises");
        assert_eq!(cpu, restored, "paused after step {pause}");

        let mark = machine.trace.len();
        let mut continued = Machine {
            bytes: machine.bytes.clone(),
            trace: Vec::new(),
        };
        for _ in pause..STEPS {
            cpu.step(&mut machine);
            restored.step(&mut continued);
        }

        assert_eq!(
            machine.trace[mark..],
            continued.trace[..],
            "the trace after step {pause} differs"
        );
        assert_eq!(cpu, restored, "the cores diverged after step {pause}");
        assert_eq!(machine.bytes, continued.bytes, "memory differs at {pause}");
    }
}

// Driving by edge and then by instruction cannot work, and the two tests below are the two halves
// of that: what it would cost, and what to do instead.
//
// `LD BC,$1234` part way through is the plain case. The hand-over carries the registers and the
// bus; it cannot carry the opcode being decoded, the operand latched so far, or how far into the
// instruction the sequencer had reached. Nor is there a moment to hand over safely: the sequencer
// commits a register write one T-state *into* the following fetch, so at the T-state that looks
// like an instruction boundary the previous instruction has not finished, and by the time it has,
// the next opcode is already read.
const PART_WAY: [u8; 6] = [0x01, 0x34, 0x12, 0x3C, 0x3C, 0x3C];

#[cfg(debug_assertions)]
#[test]
#[should_panic = "the sequencer is holding an instruction"]
fn stepping_after_driving_by_edge_is_caught() {
    let mut host = Flat::with(&PART_WAY);
    let mut cpu = Cpu::new();
    cpu.reset();
    cpu.tick(&mut host);
    cpu.tick(&mut host);
    cpu.step(&mut host);
}

#[test]
fn abandoning_the_instruction_is_what_makes_the_two_safe_to_mix() {
    let mut host = Flat::with(&PART_WAY);
    let mut cpu = Cpu::new();
    cpu.reset();
    cpu.tick(&mut host);
    cpu.tick(&mut host);
    cpu.abandon_instruction();

    for _ in 0..3 {
        cpu.step(&mut host);
    }
    let registers = cpu.registers();
    assert_eq!(registers.bc, 0x1234, "the pair was not loaded");
    assert_eq!(registers.pc, 0x0005, "three instructions did not run");
}

#[test]
fn stepping_and_then_driving_by_edge_needs_no_abandon() {
    let mut host = Flat::with(&PART_WAY);
    let mut cpu = Cpu::new();
    cpu.reset();
    cpu.step(&mut host);
    for _ in 0..8 {
        cpu.tick(&mut host);
    }
    assert_eq!(cpu.registers().bc, 0x1234, "the step did not take effect");
    cpu.abandon_instruction();
    cpu.step(&mut host);
}