Initial commit
This commit is contained in:
+137
@@ -0,0 +1,137 @@
|
||||
//! 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);
|
||||
}
|
||||
Reference in New Issue
Block a user