systemless 0.1.21

High-Level Emulation for classic Macintosh applications
Documentation
use std::path::PathBuf;
use std::time::Instant;

use systemless::cpu::Register;
use systemless::game;
use systemless::memory::MemoryBus;
use systemless::runner::{DEFAULT_REALTIME_CPU_MHZ, DEFAULT_VBL_HZ};

const DEFAULT_MAC_TIME_SECS: u32 = 3_786_912_000;
const DEFAULT_STEPS: usize = 20_000_000;
const DEFAULT_WARMUP_STEPS: usize = 500_000;

fn parse_usize_arg(args: &[String], index: usize, default: usize) -> usize {
    args.get(index)
        .and_then(|value| value.parse::<usize>().ok())
        .unwrap_or(default)
}

fn realtime_instructions_per_tick() -> u32 {
    let mhz = std::env::var("SYSTEMLESS_CPU_MHZ")
        .ok()
        .and_then(|value| value.parse::<f64>().ok())
        .unwrap_or(DEFAULT_REALTIME_CPU_MHZ);
    (mhz * 1_000_000.0 / DEFAULT_VBL_HZ).round() as u32
}

fn env_flag_enabled(name: &str, default: bool) -> bool {
    match std::env::var(name).ok().as_deref() {
        Some("1" | "true" | "True" | "TRUE" | "on" | "On" | "ON") => true,
        Some("0" | "false" | "False" | "FALSE" | "off" | "Off" | "OFF") => false,
        _ => default,
    }
}

fn run_budget(runner: &mut systemless::runner::FixtureRunner, steps: usize) -> usize {
    let mut total = 0usize;
    while total < steps && !runner.is_halted() {
        let remaining = steps - total;
        let (ran, running) = runner.run_steps(remaining, None);
        total += ran;
        if !running || ran == 0 {
            break;
        }
    }
    total
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 {
        eprintln!("Usage: {} <game.sit|app> [steps] [warmup_steps]", args[0]);
        std::process::exit(2);
    }

    let game_path = PathBuf::from(&args[1]);
    let steps = parse_usize_arg(&args, 2, DEFAULT_STEPS);
    let warmup_steps = parse_usize_arg(&args, 3, DEFAULT_WARMUP_STEPS);

    let mut runner = game::new_runner();
    runner.set_app_start_time(DEFAULT_MAC_TIME_SECS);
    runner.set_instructions_per_tick(realtime_instructions_per_tick());
    runner.set_wait_sleep_cap_in_headless(Some(0));

    let app = game::load_game_from_path(&mut runner, &game_path).unwrap_or_else(|err| {
        eprintln!("load {}: {}", game_path.display(), err);
        std::process::exit(1);
    });
    game::init_game(&mut runner, &app);

    let warmup = run_budget(&mut runner, warmup_steps);
    if let Some(spec) = std::env::var("SYSTEMLESS_BENCH_DISASM").ok() {
        if let Some((pc_s, count_s)) = spec.split_once(':') {
            let pc_s = pc_s
                .trim()
                .trim_start_matches("0x")
                .trim_start_matches("0X");
            if let (Ok(pc), Ok(count)) = (
                u32::from_str_radix(pc_s, 16),
                count_s.trim().parse::<usize>(),
            ) {
                for (addr, mnemonic, size) in runner.disassemble_at(pc, count) {
                    eprintln!(
                        "[BENCH-DISASM] ${:08X} {:<28} ; size {}",
                        addr, mnemonic, size
                    );
                }
            }
        }
    }
    if let Some(spec) = std::env::var("SYSTEMLESS_BENCH_BYTES").ok() {
        if let Some((addr_s, len_s)) = spec.split_once(':') {
            let addr_s = addr_s
                .trim()
                .trim_start_matches("0x")
                .trim_start_matches("0X");
            if let (Ok(addr), Ok(len)) = (
                u32::from_str_radix(addr_s, 16),
                len_s.trim().parse::<usize>(),
            ) {
                for row in (0..len).step_by(16) {
                    let row_len = (len - row).min(16);
                    let bytes = (0..row_len)
                        .map(|i| {
                            format!(
                                "{:02X}",
                                runner.bus().read_byte(addr + row as u32 + i as u32)
                            )
                        })
                        .collect::<Vec<_>>()
                        .join(" ");
                    eprintln!("[BENCH-BYTES] ${:08X}: {}", addr + row as u32, bytes);
                }
            }
        }
    }
    let start_tick = runner.guest_tick();
    let start_traps = runner.dispatcher().trap_count;
    let start = Instant::now();
    let measured = run_budget(&mut runner, steps);
    let elapsed = start.elapsed().as_secs_f64();
    let mips = if elapsed > 0.0 {
        measured as f64 / elapsed / 1_000_000.0
    } else {
        0.0
    };
    let end_tick = runner.guest_tick();
    let traps = runner.dispatcher().trap_count.saturating_sub(start_traps);
    let pc = runner.cpu().read_reg(Register::PC);

    println!(
        "game={} warmup={} steps={} elapsed_s={:.6} mips={:.3} ticks={} traps={} pc=${:08X} halted={} batch_simple={} fast_byte_loops={} fast_ptinrect_scan={} inline_ptinrect={}",
        game_path.display(),
        warmup,
        measured,
        elapsed,
        mips,
        end_tick.wrapping_sub(start_tick),
        traps,
        pc,
        runner.is_halted(),
        env_flag_enabled("SYSTEMLESS_BATCH_SIMPLE", false),
        env_flag_enabled("SYSTEMLESS_FAST_BYTE_LOOPS", true),
        env_flag_enabled("SYSTEMLESS_FAST_PTINRECT_SCAN", true),
        env_flag_enabled("SYSTEMLESS_INLINE_PTINRECT", true),
    );
}