hg80 1.0.0

Z80 and Z80N CPU core, stepped one clock edge at a time
Documentation
use super::*;
use crate::mcode::InstructionSet;
use crate::{BusCycle, BusRequest, Cpu, Host};
use std::prelude::rust_2024::*;

struct Watch {
    bytes: Box<[u8; 0x10000]>,
    cycles: Vec<(CycleKind, u32)>,
    opening: Option<CycleKind>,
}

fn kind_of(request: BusRequest) -> CycleKind {
    match request {
        BusRequest::OpcodeFetch { .. } | BusRequest::InterruptAcknowledge { .. } => {
            CycleKind::Fetch
        }
        BusRequest::MemoryRead { .. } => CycleKind::MemoryRead,
        BusRequest::MemoryWrite { .. } => CycleKind::MemoryWrite,
        BusRequest::PortRead { .. } => CycleKind::PortRead,
        BusRequest::PortWrite { .. } => CycleKind::PortWrite,
        _ => CycleKind::Internal,
    }
}

impl Host for Watch {
    fn read(&mut self, address: u16, _: u32) -> u8 {
        self.bytes[address as usize]
    }
    fn write(&mut self, address: u16, value: u8, _: u32) {
        self.bytes[address as usize] = value;
    }
    fn input(&mut self, _: u16, _: u32) -> u8 {
        0xFF
    }
    fn output(&mut self, _: u16, _: u8, _: u32) {}
    fn wait_states(&mut self, request: &BusRequest) -> u32 {
        self.opening = Some(kind_of(*request));
        0
    }
    fn bus_cycle(&mut self, cycle: &BusCycle) {
        self.cycles.push((kind_of(cycle.request), cycle.t_states));
    }
}

fn observed(prefix: &[u8], ir: u8, z80n: bool) -> Vec<(CycleKind, u32)> {
    let mut program = prefix.to_vec();
    program.push(ir);
    program.extend_from_slice(&[0x00; 8]);

    let mut bytes = vec![0u8; 0x10000].into_boxed_slice();
    bytes[0x8000..0x8000 + program.len()].copy_from_slice(&program);
    let mut host = Watch {
        bytes: bytes.try_into().unwrap(),
        cycles: Vec::new(),
        opening: None,
    };

    let mut cpu = Cpu::new();
    cpu.reset();
    cpu.set_z80n_enabled(z80n);
    cpu.registers_mut().sp = 0x4080;
    cpu.registers_mut().hl = 0x4000;
    cpu.registers_mut().de = 0x4010;
    cpu.registers_mut().bc = 0x4020;
    cpu.registers_mut().ix = 0x4000;
    cpu.registers_mut().iy = 0x4010;
    cpu.registers_mut().pc = 0x8000;
    cpu.abandon_instruction();

    // Two steps, because a cycle is reported when the next one opens: after one step the
    // instruction's final cycle has run but not yet closed.
    cpu.step_by_edges(&mut host);
    cpu.step_by_edges(&mut host);
    host.cycles
}

fn compare(set: InstructionSet, prefix: &[u8], ir: u8, z80n: bool) -> Option<String> {
    let planned = plan(set, ir, z80n);
    if planned.varies {
        return None;
    }
    let seen = observed(prefix, ir, z80n);
    let want: Vec<(CycleKind, u32)> = planned
        .cycles()
        .iter()
        .map(|c| (c.kind, u32::from(c.t_states)))
        .collect();
    // Each prefix byte is a fetch of its own and belongs to no plan, so the instruction's own
    // cycles start after them.
    let from = prefix.len();
    if seen.len() < from + want.len() {
        return Some(format!("saw {seen:?}, planned {want:?}"));
    }
    let tail = &seen[from..from + want.len()];
    if tail == want.as_slice() {
        None
    } else {
        Some(format!("saw {tail:?}, planned {want:?}"))
    }
}

#[test]
fn a_derived_plan_matches_what_the_core_does() {
    let mut wrong = Vec::new();
    let mut checked = 0;
    for (set, prefix) in [
        (InstructionSet::Base, &[][..]),
        (InstructionSet::Cb, &[0xCB][..]),
        (InstructionSet::Ed, &[0xED][..]),
    ] {
        for ir in 0u8..=0xFF {
            if matches!(set, InstructionSet::Base) && matches!(ir, 0xCB | 0xED | 0xDD | 0xFD) {
                continue;
            }
            if let Some(why) = compare(set, prefix, ir, false) {
                wrong.push(format!("{set:?} {ir:02X}: {why}"));
            } else {
                checked += 1;
            }
        }
    }
    assert!(
        checked > 400,
        "only {checked} instructions had a fixed shape"
    );
    assert!(
        wrong.is_empty(),
        "{} of {} disagree:\n{}",
        wrong.len(),
        wrong.len() + checked,
        wrong.join("\n")
    );
}

#[test]
fn most_instructions_have_a_single_shape() {
    let mut varying = Vec::new();
    let mut total = 0;
    for (set, z80n) in [
        (InstructionSet::Base, false),
        (InstructionSet::Cb, false),
        (InstructionSet::Ed, false),
        (InstructionSet::Ed, true),
    ] {
        for ir in 0u8..=0xFF {
            total += 1;
            if plan(set, ir, z80n).varies {
                varying.push(format!("{set:?}{} {ir:02X}", if z80n { "+n" } else { "" }));
            }
        }
    }
    assert!(
        varying.len() * 8 < total,
        "{} of {total} instructions vary, which is too many to be a small set:\n{}",
        varying.len(),
        varying.join(" ")
    );
    std::println!(
        "{} of {total} instructions vary: {}",
        varying.len(),
        varying.join(" ")
    );
}