Files
daedalian/src/headless.rs
T
2026-10-31 13:00:20 -05:00

138 lines
5.5 KiB
Rust

//! Headless runner for testing: `daedalian --headless OUT_DIR SCRIPT...`
//! Script tokens: `N` run N frames; `BUTTON:N` hold a button for N frames
//! (up down left right a b start select); `shot:NAME` save OUT_DIR/NAME.png.
//! All audio is written to OUT_DIR/audio.wav.
use crate::machine::*;
use std::fs;
use std::path::Path;
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for &b in data {
crc ^= b as u32;
for _ in 0..8 {
crc = if crc & 1 != 0 { 0xEDB8_8320 ^ (crc >> 1) } else { crc >> 1 };
}
}
!crc
}
fn write_png(path: &Path, w: usize, h: usize, gray: &[u8]) {
let mut raw = Vec::new();
for y in 0..h {
raw.push(0);
raw.extend_from_slice(&gray[y * w..(y + 1) * w]);
}
// zlib stream with stored (uncompressed) deflate blocks
let mut z = vec![0x78, 0x01];
for (i, chunk) in raw.chunks(65535).enumerate() {
let last = (i + 1) * 65535 >= raw.len();
z.push(last as u8);
z.extend_from_slice(&(chunk.len() as u16).to_le_bytes());
z.extend_from_slice(&(!(chunk.len() as u16)).to_le_bytes());
z.extend_from_slice(chunk);
}
let (mut a, mut b) = (1u32, 0u32);
for &x in &raw {
a = (a + x as u32) % 65521;
b = (b + a) % 65521;
}
z.extend_from_slice(&(b << 16 | a).to_be_bytes());
let mut png = b"\x89PNG\r\n\x1a\n".to_vec();
let mut chunk = |t: &[u8], d: &[u8]| {
png.extend_from_slice(&(d.len() as u32).to_be_bytes());
let mut td = t.to_vec();
td.extend_from_slice(d);
png.extend_from_slice(&td);
png.extend_from_slice(&crc32(&td).to_be_bytes());
};
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&(w as u32).to_be_bytes());
ihdr.extend_from_slice(&(h as u32).to_be_bytes());
ihdr.extend_from_slice(&[8, 0, 0, 0, 0]);
chunk(b"IHDR", &ihdr);
chunk(b"IDAT", &z);
chunk(b"IEND", &[]);
fs::write(path, png).unwrap();
}
fn write_wav(path: &Path, rate: u32, samples: &[f32]) {
let mut d = Vec::new();
let data_len = samples.len() as u32 * 2;
d.extend_from_slice(b"RIFF");
d.extend_from_slice(&(36 + data_len).to_le_bytes());
d.extend_from_slice(b"WAVEfmt ");
d.extend_from_slice(&16u32.to_le_bytes());
d.extend_from_slice(&1u16.to_le_bytes());
d.extend_from_slice(&2u16.to_le_bytes());
d.extend_from_slice(&rate.to_le_bytes());
d.extend_from_slice(&(rate * 4).to_le_bytes());
d.extend_from_slice(&4u16.to_le_bytes());
d.extend_from_slice(&16u16.to_le_bytes());
d.extend_from_slice(b"data");
d.extend_from_slice(&data_len.to_le_bytes());
for s in samples {
d.extend_from_slice(&((s.clamp(-1.0, 1.0) * 32767.0) as i16).to_le_bytes());
}
fs::write(path, d).unwrap();
}
pub fn run(rom: &[u8], args: &[String]) {
let out = Path::new(&args[0]);
fs::create_dir_all(out).unwrap();
let rate = 44_100;
let mut m = Machine::new(rom, rate);
let mut audio = Vec::new();
let mut frames = 0u64;
let run = |m: &mut Machine, n: u64, audio: &mut Vec<f32>, frames: &mut u64| {
for _ in 0..n {
m.run_frame();
audio.append(&mut m.apu.out);
*frames += 1;
}
};
for tok in &args[1..] {
if let Ok(n) = tok.parse::<u64>() {
run(&mut m, n, &mut audio, &mut frames);
} else if let Some(name) = tok.strip_prefix("shot:") {
let gray: Vec<u8> = m.framebuffer.iter().map(|&s| [0xFF, 0xAA, 0x55, 0x00][s as usize]).collect();
write_png(&out.join(format!("{name}.png")), SCREEN_W, SCREEN_H, &gray);
println!("frame {frames}: {name}.png (pc ${:04X})", m.pc);
} else if let Some(spec) = tok.strip_prefix("goto:") {
// steer the puzzle cursor (wPieceY/wPieceX at $C0A1/$C0A2) to a position
let (ty, tx) = spec.split_once(':').unwrap();
let (ty, tx) = (u8::from_str_radix(ty, 16).unwrap(), u8::from_str_radix(tx, 16).unwrap());
for _ in 0..100 {
let (cy, cx) = (m.r8(0xC0A1), m.r8(0xC0A2));
let bit = if cy < ty { BTN_DOWN } else if cy > ty { BTN_UP }
else if cx < tx { BTN_RIGHT } else if cx > tx { BTN_LEFT } else { break };
m.buttons |= bit;
run(&mut m, 1, &mut audio, &mut frames);
m.buttons &= !bit;
run(&mut m, 5, &mut audio, &mut frames);
}
} else if let Some(spec) = tok.strip_prefix("peek:") {
let (addr, n) = spec.split_once(':').unwrap_or((spec, "1"));
let addr = u16::from_str_radix(addr, 16).unwrap();
let vals: Vec<String> = (0..n.parse::<u16>().unwrap())
.map(|i| format!("{:02X}", m.r8(addr + i)))
.collect();
println!("frame {frames}: ${addr:04X}: {}", vals.join(" "));
} else if let Some((btn, n)) = tok.split_once(':') {
let bit = match btn {
"up" => BTN_UP, "down" => BTN_DOWN, "left" => BTN_LEFT, "right" => BTN_RIGHT,
"a" => BTN_A, "b" => BTN_B, "start" => BTN_START, "select" => BTN_SELECT,
_ => panic!("unknown token {tok}"),
};
m.buttons |= bit;
run(&mut m, n.parse().unwrap(), &mut audio, &mut frames);
m.buttons &= !bit;
} else {
panic!("unknown token {tok}");
}
}
write_wav(&out.join("audio.wav"), rate, &audio);
println!("{frames} frames, {:.1}s of audio", audio.len() as f32 / 2.0 / rate as f32);
}