#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::collections::VecDeque;
use saturnus::ADDR_MASK;
use saturnus::internals::{Chip, DatSize, Decoded, Instruction, Ptr, Reg, decode, disassemble};
use saturnus::io::Key;
use saturnus::{Machine, Model};
const DEFAULT_CYCLES: u64 = 20_000_000;
const KEY_GAP: u64 = 2_000_000;
const KEY_HOLD: u64 = 200_000;
const WATCH_PRINT_LIMIT: u64 = 20;
#[derive(Debug, Default)]
struct Args {
rom: String,
cycles: u64,
keys: Vec<(u64, Key, bool)>,
trace: usize,
watch_pc: Option<u32>,
screen: bool,
model: Option<Model>,
io_trace: u64,
}
fn usage() -> String {
"usage: boot <rom-file> [--cycles N] [--keys \"seq\"] [--trace N] [--watch-pc HEX] [--screen] \
[--model NAME] [--io-trace N]"
.to_string()
}
fn parse_keys(seq: &str) -> Result<Vec<(u64, Key, bool)>, String> {
let mut events = Vec::new();
let mut prev = 0u64;
for item in seq.split_whitespace() {
let (name, at) = match item.split_once('@') {
Some((n, c)) => (
n,
c.replace('_', "")
.parse::<u64>()
.map_err(|e| format!("bad cycle in {item:?}: {e}"))?,
),
None => (item, prev + KEY_GAP),
};
let key = Key::from_name(name).ok_or_else(|| format!("unknown key {name:?}"))?;
events.push((at, key, true));
events.push((at + KEY_HOLD, key, false));
prev = at;
}
events.sort_by_key(|&(at, _, down)| (at, down));
Ok(events)
}
fn parse_args() -> Result<Args, String> {
let mut args = Args {
cycles: DEFAULT_CYCLES,
..Args::default()
};
let mut it = std::env::args().skip(1);
let mut rom = None;
while let Some(a) = it.next() {
let mut value = |flag: &str| it.next().ok_or_else(|| format!("{flag} needs a value"));
match a.as_str() {
"--cycles" => {
args.cycles = value("--cycles")?
.replace('_', "")
.parse()
.map_err(|e| format!("--cycles: {e}"))?;
}
"--keys" => args.keys = parse_keys(&value("--keys")?)?,
"--trace" => {
args.trace = value("--trace")?
.parse()
.map_err(|e| format!("--trace: {e}"))?;
}
"--watch-pc" => {
let v = value("--watch-pc")?;
let v = v.trim_start_matches('#').trim_start_matches("0x");
args.watch_pc =
Some(u32::from_str_radix(v, 16).map_err(|e| format!("--watch-pc: {e}"))?);
}
"--screen" => args.screen = true,
"--model" => {
let v = value("--model")?;
args.model = Some(
Model::ALL
.into_iter()
.find(|m| m.name() == v)
.ok_or_else(|| format!("unknown model {v:?}"))?,
);
}
"--io-trace" => {
args.io_trace = value("--io-trace")?
.parse()
.map_err(|e| format!("--io-trace: {e}"))?;
}
"-h" | "--help" => return Err(usage()),
_ if a.starts_with("--") => return Err(format!("unknown option {a}\n{}", usage())),
_ => rom = Some(a),
}
}
args.rom = rom.ok_or_else(usage)?;
Ok(args)
}
fn print_trace(ring: &VecDeque<(u32, Decoded)>) {
if ring.is_empty() {
return;
}
println!("--- last {} instructions ---", ring.len());
for (pc, d) in ring {
println!("#{pc:05X} {}", disassemble(&d.instr));
}
}
fn reg(m: &Machine, r: Reg) -> u64 {
let g = &m.cpu.regs;
match r {
Reg::A => g.a,
Reg::B => g.b,
Reg::C => g.c,
Reg::D => g.d,
}
}
fn io_event(m: &Machine, pc: u32, i: &Instruction) -> Option<(String, Option<Reg>)> {
let c_a = m.cpu.regs.c & 0xF_FFFF;
let hdw = m.hw.mc.window(Chip::Hdw);
let in_hdw = |a: u32| hdw.is_some_and(|(base, mask)| a & mask == base);
let ptr = |p: Ptr| match p {
Ptr::D0 => m.cpu.regs.d0,
Ptr::D1 => m.cpu.regs.d1,
};
let size = |s: &DatSize| match s {
DatSize::Nibbles(n) => format!("{n}"),
DatSize::Field(f) => format!("{f:?}"),
};
let off = |a: u32| a.wrapping_sub(hdw.map_or(0, |w| w.0)) & 0x3F;
Some(match i {
Instruction::Config => (format!("#{pc:05X} CONFIG #{c_a:05X}"), None),
Instruction::Uncnfg => (format!("#{pc:05X} UNCNFG #{c_a:05X}"), None),
Instruction::CId => (format!("#{pc:05X} C=ID"), Some(Reg::C)),
Instruction::OutC | Instruction::OutCs => (
format!("#{pc:05X} OUT=C #{:03X}", m.cpu.regs.c & 0xFFF),
None,
),
Instruction::In { dst } => (
format!("#{pc:05X} IN (OUT #{:03X})", m.hw.out()),
Some(*dst),
),
Instruction::DatRead {
dst,
ptr: p,
size: s,
} if in_hdw(ptr(*p)) => (
format!("#{pc:05X} read #1{:02X} size {}", off(ptr(*p)), size(s)),
Some(*dst),
),
Instruction::DatWrite {
src,
ptr: p,
size: s,
} if in_hdw(ptr(*p)) => (
format!(
"#{pc:05X} write #1{:02X} size {} value #{:016X}",
off(ptr(*p)),
size(s),
reg(m, *src)
),
None,
),
_ => return None,
})
}
fn print_state(m: &Machine) {
let r = &m.cpu.regs;
let t = &m.hw.io.timers;
println!("pc #{:05X}", r.pc);
println!("cycles {}", m.cycles());
println!("shutdown {}", m.is_shutdown());
println!("in_interrupt {}", r.in_interrupt);
println!("interrupts_enabled {}", r.interrupts_enabled);
println!("memory controller {:?}", m.hw.mc);
println!(
"TIMER1 #{:X} ctrl #{:X}",
t.t1,
t.read_t1_ctrl()
);
println!(
"TIMER2 #{:08X} ctrl #{:X}",
t.t2,
t.read_t2_ctrl()
);
println!("OUT #{:03X}", m.hw.out());
}
fn main() {
let args = match parse_args() {
Ok(a) => a,
Err(e) => {
eprintln!("{e}");
std::process::exit(2);
}
};
let rom =
std::fs::read(&args.rom).unwrap_or_else(|e| panic!("cannot read ROM {}: {e}", args.rom));
let model = args.model.unwrap_or_else(|| {
Model::ALL
.into_iter()
.find(|m| m.rom_bytes() == rom.len())
.unwrap_or(Model::Hp48sx)
});
let mut m = Machine::new(model, &rom).unwrap_or_else(|e| panic!("{e}"));
let mut events: VecDeque<_> = args.keys.iter().copied().collect();
let mut ring: VecDeque<(u32, Decoded)> = VecDeque::with_capacity(args.trace);
let mut watch_hits = 0u64;
let mut halted = None;
let mut io_lines = 0u64;
while m.cycles() < args.cycles {
while let Some(&(at, key, down)) = events.front() {
if at > m.cycles() {
break;
}
if down {
m.key_down(key).unwrap();
} else {
m.key_up(key).unwrap();
}
events.pop_front();
}
if !m.is_shutdown() {
let pc = m.cpu.regs.pc;
if args.watch_pc == Some(pc) {
watch_hits += 1;
if watch_hits <= WATCH_PRINT_LIMIT {
println!("watch #{pc:05X} hit {watch_hits} at cycle {}", m.cycles());
}
}
if args.trace > 0 {
if ring.len() == args.trace {
ring.pop_front();
}
ring.push_back((pc, decode(|a| m.peek(a & ADDR_MASK), pc)));
}
}
let mut after = None;
if io_lines < args.io_trace && !m.is_shutdown() {
let pc = m.cpu.regs.pc;
let d = decode(|a| m.peek(a & ADDR_MASK), pc);
if let Some((line, r)) = io_event(&m, pc, &d.instr) {
io_lines += 1;
if r.is_some() {
print!("{line}");
after = r;
} else {
println!("{line}");
}
}
}
let until = events.front().map_or(args.cycles, |e| e.0.min(args.cycles));
let result = m
.step_for(until.saturating_sub(m.cycles()).max(1))
.map(|_| ());
if let Some(r) = after {
println!(" -> #{:016X} (cycle {})", reg(&m, r), m.cycles());
}
if let Err(h) = result {
halted = Some(h);
break;
}
}
if let Some(h) = halted {
println!("HALT: {h}");
}
if args.watch_pc.is_some() {
println!("watch hits {watch_hits}");
}
print_trace(&ring);
print_state(&m);
if args.screen {
print!("{}", m.lcd().to_text());
}
}