use super::*;
use crate::{BusCycle, BusRequest, Cpu, Host, InterruptMode, Registers, Z80nCommand};
use std::prelude::rust_2024::*;
#[derive(Clone, PartialEq, Eq, Debug)]
enum Event {
Opening(&'static str, u16),
Closed(&'static str, u32),
Fetch(u16, u32, u8),
Read(u16, u32, u8),
Write(u16, u32, u8),
Vector(u8),
Token(Z80nCommand, 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",
}
}
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,
}
}
struct Watch {
bytes: Box<[u8; 0x10000]>,
events: Vec<Event>,
waits: u32,
vector: u8,
}
impl Host for Watch {
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, _: u16, _: u32) -> u8 {
0xFF
}
fn output(&mut self, _: u16, _: u8, _: u32) {}
fn wait_states(&mut self, request: &BusRequest) -> u32 {
self.events
.push(Event::Opening(kind_of(*request), address_of(*request)));
self.waits
}
fn z80n_command(&mut self, command: Z80nCommand, data: u16) {
self.events.push(Event::Token(command, data));
}
fn interrupt_vector(&mut self) -> u8 {
self.events.push(Event::Vector(self.vector));
self.vector
}
fn bus_cycle(&mut self, cycle: &BusCycle) {
self.events
.push(Event::Closed(kind_of(cycle.request), cycle.t_states));
}
}
const PROGRAM: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/programs/stepped.bin"
));
const ORIGIN: u16 = 0x8000;
fn memory() -> Box<[u8; 0x10000]> {
let mut bytes = vec![0u8; 0x10000].into_boxed_slice();
let at = ORIGIN as usize;
bytes[at..at + PROGRAM.len()].copy_from_slice(PROGRAM);
for offset in 0..0x40usize {
bytes[0x4000 + offset] = offset.to_le_bytes()[0].wrapping_mul(7).wrapping_add(3);
}
for target in (0..8usize).map(|n| n * 8) {
bytes[target] = 0xC9;
}
bytes.try_into().unwrap()
}
const STATES: [(u16, u16, u16, u16); 4] = [
(0x1234, 0x4008, 0x4014, 0x4020),
(0x00FF, 0x0000, 0xFFFF, 0x4000),
(0xFF00, 0x8001, 0x7FFF, 0x403F),
(0x8028, 0xFFFF, 0x0001, 0x4021),
];
fn seeded_as(registers: &mut Registers, state: usize) {
seeded(registers);
let (af, bc, de, hl) = STATES[state];
registers.af = af;
registers.bc = bc;
registers.de = de;
registers.hl = hl;
}
fn seeded(registers: &mut Registers) {
registers.af_alt = 0x9876;
registers.bc_alt = 0x1111;
registers.de_alt = 0x2222;
registers.hl_alt = 0x3333;
registers.ix = 0x4030;
registers.iy = 0x4038;
registers.wz = 0x0000;
registers.af = 0x1234;
registers.bc = 0x4008;
registers.de = 0x4014;
registers.hl = 0x4020;
registers.sp = 0x4080;
registers.pc = 0x8000;
registers.i = 0x7E;
registers.r = 0x11;
}
fn beyond() -> u16 {
ORIGIN + u16::try_from(PROGRAM.len()).expect("the sweep fits in the address space")
}
fn by_reference(waits: u32) -> (Vec<Event>, Registers) {
let mut host = Watch {
bytes: memory(),
events: Vec::new(),
waits,
vector: 0xFF,
};
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(true);
seeded(cpu.registers_mut());
cpu.abandon_instruction();
let mut ran = 0;
while cpu.registers().pc < beyond() {
cpu.step_by_edges(&mut host);
ran += 1;
assert!(ran < 2000, "the reference did not leave the program");
}
(host.events, *cpu.registers())
}
fn by_stepped(waits: u32) -> (Vec<Event>, Registers) {
let mut host = Watch {
bytes: memory(),
events: Vec::new(),
waits,
vector: 0xFF,
};
let mut stepped = Stepped::new();
stepped.set_z80n_enabled(true);
seeded(&mut stepped.registers);
while stepped.registers.pc < beyond() {
let at = stepped.registers.pc;
assert_ne!(
stepped.step(&mut host),
Ran::NotYet,
"the instruction at {at:04X} is not implemented by the stepped layer yet"
);
}
(host.events, stepped.registers)
}
#[test]
fn the_stepped_layer_agrees_with_the_reference_on_what_it_implements() {
for waits in [0u32, 1, 3] {
let (reference, reference_registers) = by_reference(waits);
let (stepped, stepped_registers) = by_stepped(waits);
for (index, (a, b)) in reference.iter().zip(stepped.iter()).enumerate() {
assert_eq!(a, b, "event {index} at {waits} waits");
}
assert_eq!(
stepped.len() + 2,
reference.len(),
"the stepped layer produced {} events against {}",
stepped.len(),
reference.len()
);
assert_eq!(
reference_registers, stepped_registers,
"registers at {waits} waits"
);
}
}
struct Table {
name: &'static str,
prefix: &'static [u8],
opcode_at: usize,
z80n: bool,
}
const fn table(name: &'static str, prefix: &'static [u8], opcode_at: usize) -> Table {
Table {
name,
prefix,
opcode_at,
z80n: false,
}
}
const fn z80n(name: &'static str, prefix: &'static [u8], opcode_at: usize) -> Table {
Table {
name,
prefix,
opcode_at,
z80n: true,
}
}
const ISOLATED: [Table; 7] = [
z80n("Z80N", &[0xED], 1),
table("base", &[], 0),
table("CB", &[0xCB], 1),
table("ED", &[0xED], 1),
table("DD", &[0xDD], 1),
table("FD", &[0xFD], 1),
table("DD CB", &[0xDD, 0xCB], 3),
];
const COVERED: [Table; 6] = [
z80n("Z80N", &[0xED], 1),
table("base", &[], 0),
table("CB", &[0xCB], 1),
table("ED", &[0xED], 1),
table("DD", &[0xDD], 1),
table("DD CB", &[0xDD, 0xCB], 3),
];
fn skipped(table: &Table, opcode: u8) -> bool {
match table.name {
"base" => matches!(opcode, 0xCB | 0xDD | 0xED | 0xFD),
"DD" | "FD" => matches!(opcode, 0xDD | 0xFD | 0xED | 0xCB),
_ => false,
}
}
fn planted(bytes: &mut [u8; 0x10000], table: &Table, opcode: u8) {
let at = ORIGIN as usize;
for (offset, byte) in bytes.iter_mut().enumerate().skip(at).take(12) {
*byte = if offset % 2 == 0 { 0x40 } else { 0x02 };
}
bytes[at..at + table.prefix.len()].copy_from_slice(table.prefix);
bytes[at + table.opcode_at] = opcode;
}
fn against_the_plain_form(opcode: u8, prefix: u8) -> bool {
(0..STATES.len()).all(|state| plain_form_at(opcode, prefix, state))
}
fn plain_form_at(opcode: u8, prefix: u8, state: usize) -> bool {
let run = |prefixed: bool| {
let mut bytes = memory();
let at = ORIGIN as usize;
for (offset, byte) in bytes.iter_mut().enumerate().skip(at).take(12) {
*byte = if offset % 2 == 0 { 0x40 } else { 0x02 };
}
if prefixed {
bytes[at] = prefix;
}
bytes[at + 1] = opcode;
let mut host = Watch {
bytes,
events: Vec::new(),
waits: 0,
vector: 0xFF,
};
let mut cpu = Cpu::new();
cpu.reset();
seeded_as(cpu.registers_mut(), state);
cpu.registers_mut().pc = if prefixed { ORIGIN } else { ORIGIN + 1 };
cpu.registers_mut().r = if prefixed { 0x10 } else { 0x11 };
cpu.abandon_instruction();
cpu.step_by_edges(&mut host);
(host.events, *cpu.registers())
};
let (plain, plain_registers) = run(false);
let (prefixed, prefixed_registers) = run(true);
prefixed.len() > 3 && prefixed[3..] == plain[..] && prefixed_registers == plain_registers
}
fn takes_the_extended_path(opcode: u8) -> bool {
let mut bytes = memory();
planted(&mut bytes, &z80n("Z80N", &[0xED], 1), opcode);
let mut host = Watch {
bytes,
events: Vec::new(),
waits: 0,
vector: 0xFF,
};
let mut stepped = Stepped::new();
stepped.set_z80n_enabled(true);
seeded(&mut stepped.registers);
stepped.step(&mut host);
was_seen(&stepped.seen.z80n, opcode)
}
fn against_the_extended_no_op(opcode: u8) -> bool {
(0..STATES.len()).all(|state| extended_no_op_at(opcode, state))
}
fn extended_no_op_at(opcode: u8, state: usize) -> bool {
let run = |opcode: u8| {
let mut bytes = memory();
planted(&mut bytes, &table("ED", &[0xED], 1), opcode);
let mut host = Watch {
bytes,
events: Vec::new(),
waits: 0,
vector: 0xFF,
};
let mut cpu = Cpu::new();
cpu.reset();
seeded_as(cpu.registers_mut(), state);
cpu.abandon_instruction();
cpu.step_by_edges(&mut host);
let events: Vec<Event> = host
.events
.into_iter()
.map(|event| match event {
Event::Fetch(address, at, _) => Event::Fetch(address, at, 0),
other => other,
})
.collect();
(events, *cpu.registers())
};
run(opcode) == run(0x00)
}
fn implements(table: &Table, opcode: u8) -> bool {
let mut bytes: Box<[u8; 0x10000]> = vec![0u8; 0x10000]
.into_boxed_slice()
.try_into()
.expect("the address space is that size");
planted(&mut bytes, table, opcode);
let mut host = Watch {
bytes,
events: Vec::new(),
waits: 0,
vector: 0xFF,
};
let mut stepped = Stepped::new();
stepped.set_z80n_enabled(table.z80n);
seeded(&mut stepped.registers);
stepped.step(&mut host) != Ran::NotYet
}
fn in_isolation(
table: &Table,
opcode: u8,
waits: u32,
state: usize,
reference: bool,
) -> (Vec<Event>, Registers) {
let mut bytes = memory();
planted(&mut bytes, table, opcode);
let mut host = Watch {
bytes,
events: Vec::new(),
waits,
vector: 0xFF,
};
if reference {
let mut cpu = Cpu::new();
cpu.reset();
cpu.set_z80n_enabled(table.z80n);
seeded_as(cpu.registers_mut(), state);
cpu.abandon_instruction();
cpu.step_by_edges(&mut host);
(host.events, *cpu.registers())
} else {
let mut stepped = Stepped::new();
stepped.set_z80n_enabled(table.z80n);
seeded_as(&mut stepped.registers, state);
stepped.step(&mut host);
(host.events, stepped.registers)
}
}
#[test]
fn every_implemented_instruction_agrees_in_isolation() {
let mut checked = 0;
let mut wrong = Vec::new();
let mut unimplemented = Vec::new();
for table in &ISOLATED {
for opcode in 0u8..=0xFF {
if skipped(table, opcode) {
continue;
}
if !implements(table, opcode) {
unimplemented.push(format!("{} {opcode:02X}", table.name));
continue;
}
for (waits, state) in [(0u32, 0), (2, 1), (0, 2), (1, 3)] {
let (want, want_registers) = in_isolation(table, opcode, waits, state, true);
let (got, got_registers) = in_isolation(table, opcode, waits, state, false);
let mut fault = None;
if want.len() != got.len() + 2 {
fault = Some(format!(
"{} events against {}, where the reference leads by two",
got.len(),
want.len()
));
}
for (index, (a, b)) in want.iter().zip(got.iter()).enumerate() {
if fault.is_some() {
break;
}
if a != b {
fault = Some(format!("event {index}: want {a:?}, got {b:?}"));
break;
}
}
if fault.is_none() && want_registers != got_registers {
fault = Some(format!(
"registers: want {want_registers:?}, got {got_registers:?}"
));
}
if let Some(why) = fault {
let name = table.name;
wrong.push(format!(
"{name} {opcode:02X} at {waits} waits, state {state} — {why}"
));
} else {
checked += 1;
}
}
}
}
assert!(
wrong.is_empty(),
"{} of {} disagree; first five:\n{}",
wrong.len(),
wrong.len() + checked,
wrong.iter().take(5).cloned().collect::<Vec<_>>().join("\n")
);
assert!(checked > 5000, "only {checked} comparisons ran");
assert!(
unimplemented.is_empty(),
"the layer does not implement {}",
unimplemented.join(", ")
);
}
fn seen_in(seen: &Seen, table: &Table, opcode: u8) -> bool {
let bits = match table.name {
"base" => &seen.base,
"CB" => &seen.bits,
"ED" => &seen.extended,
"Z80N" => &seen.z80n,
"DD" | "FD" => &seen.indexed,
_ => &seen.indexed_bits,
};
was_seen(bits, opcode)
}
#[test]
fn the_sweep_does_not_shrink() {
let mut host = Watch {
bytes: memory(),
events: Vec::new(),
waits: 0,
vector: 0xFF,
};
let mut stepped = Stepped::new();
stepped.set_z80n_enabled(true);
seeded(&mut stepped.registers);
while stepped.registers.pc < beyond() {
stepped.step(&mut host);
}
let mut reached = 0;
let mut implemented = 0;
let mut missing = Vec::new();
let mut inert = 0;
for table in &COVERED {
for opcode in 0u8..=0xFF {
if skipped(table, opcode) || opcode == 0x76 || !implements(table, opcode) {
continue;
}
let same_as_one_already_covered = match table.name {
"DD" | "FD" => against_the_plain_form(opcode, 0xDD),
"ED" => opcode != 0x00 && against_the_extended_no_op(opcode),
"Z80N" => !takes_the_extended_path(opcode),
_ => false,
};
if same_as_one_already_covered {
inert += 1;
continue;
}
implemented += 1;
if seen_in(&stepped.seen, table, opcode) {
reached += 1;
} else if missing.len() < 24 {
missing.push(format!("{} {opcode:02X}", table.name));
}
}
}
std::println!(
"sweep reaches {reached} of {implemented} implemented instructions, and {inert} more run something already covered"
);
assert_eq!(
reached,
implemented,
"the sweep no longer reaches every instruction the layer implements; missing {}",
missing.join(", ")
);
}
#[derive(Clone, Copy)]
enum Line {
Maskable,
NonMaskable,
}
struct Accepting {
code: &'static [u8],
mode: InterruptMode,
enabled: bool,
line: Line,
raise_before: usize,
steps: usize,
vector: u8,
}
const HANDLER: u16 = 0x4040;
fn interrupted(case: &Accepting, waits: u32, reference: bool) -> (Vec<Event>, Registers) {
let mut bytes = memory();
let at = ORIGIN as usize;
bytes[at..at + case.code.len()].copy_from_slice(case.code);
let table = usize::from(u16::from_be_bytes([0x7E, case.vector]));
bytes[table..table + 2].copy_from_slice(&HANDLER.to_le_bytes());
for entry in [usize::from(HANDLER), 0x0066] {
bytes[entry] = 0x3C;
bytes[entry + 1] = 0xED;
bytes[entry + 2] = 0x4D;
}
let mut host = Watch {
bytes,
events: Vec::new(),
waits,
vector: case.vector,
};
macro_rules! run {
($core:expr, $step:ident, $raise:expr) => {{
for index in 0..case.steps {
if index == case.raise_before {
$raise;
}
$core.$step(&mut host);
}
}};
}
if reference {
let mut cpu = Cpu::new();
cpu.reset();
seeded(cpu.registers_mut());
cpu.registers_mut().interrupt_mode = case.mode;
cpu.registers_mut().iff1 = case.enabled;
cpu.registers_mut().iff2 = case.enabled;
cpu.abandon_instruction();
run!(
cpu,
step_by_edges,
match case.line {
Line::Maskable => cpu.set_interrupt_requested(true),
Line::NonMaskable => cpu.request_nmi(),
}
);
(host.events, *cpu.registers())
} else {
let mut stepped = Stepped::new();
seeded(&mut stepped.registers);
stepped.registers.interrupt_mode = case.mode;
stepped.registers.iff1 = case.enabled;
stepped.registers.iff2 = case.enabled;
run!(
stepped,
step,
match case.line {
Line::Maskable => stepped.set_interrupt_requested(true),
Line::NonMaskable => stepped.set_nmi_requested(true),
}
);
(host.events, stepped.registers)
}
}
fn agrees_on(case: &Accepting) {
for waits in [0u32, 1, 2] {
let (want, want_registers) = interrupted(case, waits, true);
let (got, got_registers) = interrupted(case, waits, false);
for (index, (a, b)) in want.iter().zip(got.iter()).enumerate() {
assert_eq!(a, b, "event {index} at {waits} waits");
}
assert_eq!(
got.len() + 2,
want.len(),
"the stepped layer produced {} events against {}",
got.len(),
want.len()
);
assert_eq!(want_registers, got_registers, "registers at {waits} waits");
}
}
const RUNNING: &[u8] = &[0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00];
const HALTING: &[u8] = &[0x00, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
#[test]
fn an_accepted_interrupt_in_mode_zero_runs_the_byte_the_device_supplies() {
for vector in [0xFFu8, 0xC7, 0x3C] {
agrees_on(&Accepting {
code: RUNNING,
mode: InterruptMode::Mode0,
enabled: true,
line: Line::Maskable,
raise_before: 1,
steps: 6,
vector,
});
}
}
#[test]
fn an_accepted_interrupt_in_mode_one_restarts_at_the_fixed_address() {
agrees_on(&Accepting {
code: RUNNING,
mode: InterruptMode::Mode1,
enabled: true,
line: Line::Maskable,
raise_before: 1,
steps: 6,
vector: 0x22,
});
}
#[test]
fn an_accepted_interrupt_in_mode_two_reads_its_target_from_the_table() {
for vector in [0x22u8, 0xFF] {
agrees_on(&Accepting {
code: RUNNING,
mode: InterruptMode::Mode2,
enabled: true,
line: Line::Maskable,
raise_before: 1,
steps: 6,
vector,
});
}
}
#[test]
fn a_line_raised_with_interrupts_disabled_is_ignored() {
agrees_on(&Accepting {
code: RUNNING,
mode: InterruptMode::Mode1,
enabled: false,
line: Line::Maskable,
raise_before: 1,
steps: 6,
vector: 0x22,
});
}
#[test]
fn the_nonmaskable_line_is_accepted_whether_interrupts_are_enabled_or_not() {
for enabled in [false, true] {
agrees_on(&Accepting {
code: RUNNING,
mode: InterruptMode::Mode1,
enabled,
line: Line::NonMaskable,
raise_before: 2,
steps: 6,
vector: 0x22,
});
}
}
#[test]
fn a_halted_machine_keeps_fetching_until_a_line_lets_it_out() {
for line in [Line::Maskable, Line::NonMaskable] {
for raise_before in [2usize, 4] {
agrees_on(&Accepting {
code: HALTING,
mode: InterruptMode::Mode2,
enabled: true,
line,
raise_before,
steps: 8,
vector: 0x22,
});
}
}
}
#[test]
fn an_enable_holds_acceptance_off_for_one_instruction() {
agrees_on(&Accepting {
code: &[0x00, 0xFB, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00],
mode: InterruptMode::Mode1,
enabled: false,
line: Line::Maskable,
raise_before: 0,
steps: 6,
vector: 0x22,
});
}
#[test]
fn a_line_raised_inside_a_prefixed_instruction_waits_for_its_end() {
agrees_on(&Accepting {
code: &[0xDD, 0x34, 0x02, 0xFD, 0x23, 0x00, 0x00, 0x00],
mode: InterruptMode::Mode1,
enabled: true,
line: Line::Maskable,
raise_before: 1,
steps: 6,
vector: 0x22,
});
}